1
2
3
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
46
47
48 public class ModelDetector {
49 @NonNull
50 private final IBindingContext bindingContext;
51 @NonNull
52 private final IConfiguration<DeserializationFeature<?>> configuration;
53
54
55
56
57
58
59
60 public ModelDetector(
61 @NonNull IBindingContext bindingContext) {
62 this(bindingContext, new DefaultConfiguration<>());
63 }
64
65
66
67
68
69
70
71
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
96
97
98
99
100
101
102
103
104
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 if (retval == null) {
188 throw new IOException("Unrecognized JSON field name: " + name);
189 }
190 break outer;
191 }
192
193 parser.nextToken();
194
195 }
196 return retval;
197 }
198
199 public static final class Result implements Closeable {
200 @NonNull
201 private final Class<? extends IBoundObject> boundClass;
202 @Owning
203 private InputStream dataStream;
204
205 private Result(
206 @NonNull Class<? extends IBoundObject> clazz,
207 @NonNull InputStream is,
208 @NonNull byte[] buf) {
209 this.boundClass = clazz;
210 this.dataStream = new MergedStream(null, is, buf, 0, buf.length);
211 }
212
213
214
215
216
217
218 @NonNull
219 public Class<? extends IBoundObject> getBoundClass() {
220 return boundClass;
221 }
222
223
224
225
226
227
228
229 @NonNull
230 @Owning
231 public InputStream getDataStream() {
232 return ObjectUtils.requireNonNull(dataStream, "data stream already closed");
233 }
234
235 @SuppressWarnings("PMD.NullAssignment")
236 @Override
237 public void close() throws IOException {
238 this.dataStream.close();
239 this.dataStream = null;
240 }
241 }
242 }