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.ctc.wstx.stax.WstxInputFactory;
9   import com.fasterxml.jackson.core.JsonParser;
10  import com.fasterxml.jackson.core.JsonToken;
11  import com.fasterxml.jackson.core.io.MergedStream;
12  import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
13  
14  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
15  import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
16  import gov.nist.secauto.metaschema.core.model.IBoundObject;
17  import gov.nist.secauto.metaschema.core.model.util.JsonUtil;
18  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19  import gov.nist.secauto.metaschema.databind.IBindingContext;
20  import gov.nist.secauto.metaschema.databind.io.json.JsonFactoryFactory;
21  import gov.nist.secauto.metaschema.databind.io.yaml.impl.YamlFactoryFactory;
22  
23  import org.codehaus.stax2.XMLEventReader2;
24  import org.codehaus.stax2.XMLInputFactory2;
25  import org.eclipse.jdt.annotation.NotOwning;
26  import org.eclipse.jdt.annotation.Owning;
27  
28  import java.io.ByteArrayInputStream;
29  import java.io.Closeable;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.InputStreamReader;
33  import java.io.Reader;
34  import java.nio.charset.Charset;
35  
36  import javax.xml.namespace.QName;
37  import javax.xml.stream.XMLInputFactory;
38  import javax.xml.stream.XMLStreamException;
39  import javax.xml.stream.events.StartElement;
40  
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  import edu.umd.cs.findbugs.annotations.Nullable;
43  
44  /**
45   * Provides a means to analyze content to determine what type of bound data it
46   * contains.
47   */
48  public class ModelDetector {
49    @NonNull
50    private final IBindingContext bindingContext;
51    @NonNull
52    private final IConfiguration<DeserializationFeature<?>> configuration;
53  
54    /**
55     * Construct a new format detector using the default configuration.
56     *
57     * @param bindingContext
58     *          information about how Java classes are bound to Module definitions
59     */
60    public ModelDetector(
61        @NonNull IBindingContext bindingContext) {
62      this(bindingContext, new DefaultConfiguration<>());
63    }
64  
65    /**
66     * Construct a new format detector using the provided {@code configuration}.
67     *
68     * @param bindingContext
69     *          information about how Java classes are bound to Module definitions
70     * @param configuration
71     *          the deserialization configuration
72     */
73    public ModelDetector(
74        @NonNull IBindingContext bindingContext,
75        @NonNull IConfiguration<DeserializationFeature<?>> configuration) {
76      this.bindingContext = bindingContext;
77      this.configuration = configuration;
78    }
79  
80    private int getLookaheadLimit() {
81      return configuration.get(DeserializationFeature.FORMAT_DETECTION_LOOKAHEAD_LIMIT);
82    }
83  
84    @NonNull
85    private IBindingContext getBindingContext() {
86      return bindingContext;
87    }
88  
89    @NonNull
90    private IConfiguration<DeserializationFeature<?>> getConfiguration() {
91      return configuration;
92    }
93  
94    /**
95     * Analyzes the data from the provided {@code inputStream} to determine it's
96     * model.
97     *
98     * @param inputStream
99     *          the resource stream to analyze
100    * @param format
101    *          the expected format of the data to read
102    * @return the analysis result
103    * @throws IOException
104    *           if an error occurred while reading the resource
105    */
106   @NonNull
107   @Owning
108   public Result detect(@NonNull @NotOwning InputStream inputStream, @NonNull Format format)
109       throws IOException {
110     byte[] buf = ObjectUtils.notNull(inputStream.readNBytes(getLookaheadLimit()));
111 
112     Class<? extends IBoundObject> clazz;
113     try (InputStream bis = new ByteArrayInputStream(buf)) {
114       assert bis != null;
115       switch (format) {
116       case JSON:
117         try (JsonParser parser = JsonFactoryFactory.instance().createParser(bis)) {
118           assert parser != null;
119           clazz = detectModelJsonClass(parser);
120         }
121         break;
122       case YAML:
123         YAMLFactory factory = YamlFactoryFactory.newParserFactoryInstance(getConfiguration());
124         try (JsonParser parser = factory.createParser(bis)) {
125           assert parser != null;
126           clazz = detectModelJsonClass(parser);
127         }
128         break;
129       case XML:
130         clazz = detectModelXmlClass(bis);
131         break;
132       default:
133         throw new UnsupportedOperationException(
134             String.format("The format '%s' dataStream not supported", format));
135       }
136     }
137 
138     if (clazz == null) {
139       throw new IllegalStateException(
140           String.format("Detected format '%s', but unable to detect the bound data type", format.name()));
141     }
142 
143     return new Result(clazz, inputStream, buf);
144   }
145 
146   @NonNull
147   private Class<? extends IBoundObject> detectModelXmlClass(@NonNull InputStream is) throws IOException {
148     QName startElementQName;
149     try {
150       XMLInputFactory2 xmlInputFactory = (XMLInputFactory2) XMLInputFactory.newInstance();
151       assert xmlInputFactory instanceof WstxInputFactory;
152       xmlInputFactory.configureForXmlConformance();
153       xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, false);
154 
155       Reader reader = new InputStreamReader(is, Charset.forName("UTF8"));
156       XMLEventReader2 eventReader = (XMLEventReader2) xmlInputFactory.createXMLEventReader(reader);
157       while (eventReader.hasNext() && !eventReader.peek().isStartElement()) {
158         eventReader.nextEvent();
159       }
160 
161       if (!eventReader.peek().isStartElement()) {
162         throw new IOException("Unable to detect a start element");
163       }
164 
165       StartElement start = eventReader.nextEvent().asStartElement();
166       startElementQName = ObjectUtils.notNull(start.getName());
167     } catch (XMLStreamException ex) {
168       throw new IOException(ex);
169     }
170 
171     Class<? extends IBoundObject> clazz = getBindingContext().getBoundClassForRootXmlQName(startElementQName);
172     if (clazz == null) {
173       throw new IOException("Unrecognized element name: " + startElementQName.toString());
174     }
175     return clazz;
176   }
177 
178   @Nullable
179   private Class<? extends IBoundObject> detectModelJsonClass(@NonNull JsonParser parser) throws IOException {
180     Class<? extends IBoundObject> retval = null;
181     JsonUtil.advanceAndAssert(parser, JsonToken.START_OBJECT);
182     outer: while (JsonToken.FIELD_NAME.equals(parser.nextToken())) {
183       String name = ObjectUtils.notNull(parser.currentName());
184       if (!"$schema".equals(name)) {
185         IBindingContext bindingContext = getBindingContext();
186         retval = bindingContext.getBoundClassForRootJsonName(name);
187         break outer;
188       }
189       // do nothing
190       parser.nextToken();
191       // JsonUtil.skipNextValue(parser);
192     }
193     return retval;
194   }
195 
196   public static final class Result implements Closeable {
197     @NonNull
198     private final Class<? extends IBoundObject> boundClass;
199     @Owning
200     private InputStream dataStream;
201 
202     private Result(
203         @NonNull Class<? extends IBoundObject> clazz,
204         @NonNull InputStream is,
205         @NonNull byte[] buf) {
206       this.boundClass = clazz;
207       this.dataStream = new MergedStream(null, is, buf, 0, buf.length);
208     }
209 
210     /**
211      * Get the Java class representing the detected bound object.
212      *
213      * @return the Java class
214      */
215     @NonNull
216     public Class<? extends IBoundObject> getBoundClass() {
217       return boundClass;
218     }
219 
220     /**
221      * Get an {@link InputStream} that can be used to read the analyzed data from
222      * the start.
223      *
224      * @return the stream
225      */
226     @NonNull
227     @Owning
228     public InputStream getDataStream() {
229       return ObjectUtils.requireNonNull(dataStream, "data stream already closed");
230     }
231 
232     @Override
233     public void close() throws IOException {
234       this.dataStream.close();
235       this.dataStream = null;
236     }
237   }
238 }