001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.databind.io; 007 008import com.ctc.wstx.stax.WstxInputFactory; 009import com.fasterxml.jackson.core.JsonParser; 010import com.fasterxml.jackson.core.JsonToken; 011import com.fasterxml.jackson.core.io.MergedStream; 012import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 013 014import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration; 015import gov.nist.secauto.metaschema.core.configuration.IConfiguration; 016import gov.nist.secauto.metaschema.core.model.IBoundObject; 017import gov.nist.secauto.metaschema.core.model.util.JsonUtil; 018import gov.nist.secauto.metaschema.core.util.ObjectUtils; 019import gov.nist.secauto.metaschema.databind.IBindingContext; 020import gov.nist.secauto.metaschema.databind.io.json.JsonFactoryFactory; 021import gov.nist.secauto.metaschema.databind.io.yaml.impl.YamlFactoryFactory; 022 023import org.codehaus.stax2.XMLEventReader2; 024import org.codehaus.stax2.XMLInputFactory2; 025import org.eclipse.jdt.annotation.NotOwning; 026import org.eclipse.jdt.annotation.Owning; 027 028import java.io.ByteArrayInputStream; 029import java.io.Closeable; 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.InputStreamReader; 033import java.io.Reader; 034import java.nio.charset.Charset; 035 036import javax.xml.namespace.QName; 037import javax.xml.stream.XMLInputFactory; 038import javax.xml.stream.XMLStreamException; 039import javax.xml.stream.events.StartElement; 040 041import edu.umd.cs.findbugs.annotations.NonNull; 042import edu.umd.cs.findbugs.annotations.Nullable; 043 044/** 045 * Provides a means to analyze content to determine what type of bound data it 046 * contains. 047 */ 048public class ModelDetector { 049 @NonNull 050 private final IBindingContext bindingContext; 051 @NonNull 052 private final IConfiguration<DeserializationFeature<?>> configuration; 053 054 /** 055 * Construct a new format detector using the default configuration. 056 * 057 * @param bindingContext 058 * information about how Java classes are bound to Module definitions 059 */ 060 public ModelDetector( 061 @NonNull IBindingContext bindingContext) { 062 this(bindingContext, new DefaultConfiguration<>()); 063 } 064 065 /** 066 * Construct a new format detector using the provided {@code configuration}. 067 * 068 * @param bindingContext 069 * information about how Java classes are bound to Module definitions 070 * @param configuration 071 * the deserialization configuration 072 */ 073 public ModelDetector( 074 @NonNull IBindingContext bindingContext, 075 @NonNull IConfiguration<DeserializationFeature<?>> configuration) { 076 this.bindingContext = bindingContext; 077 this.configuration = configuration; 078 } 079 080 private int getLookaheadLimit() { 081 return configuration.get(DeserializationFeature.FORMAT_DETECTION_LOOKAHEAD_LIMIT); 082 } 083 084 @NonNull 085 private IBindingContext getBindingContext() { 086 return bindingContext; 087 } 088 089 @NonNull 090 private IConfiguration<DeserializationFeature<?>> getConfiguration() { 091 return configuration; 092 } 093 094 /** 095 * Analyzes the data from the provided {@code inputStream} to determine it's 096 * model. 097 * 098 * @param inputStream 099 * 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 if (retval == null) { 188 throw new IOException("Unrecognized JSON field name: " + name); 189 } 190 break outer; 191 } 192 // do nothing 193 parser.nextToken(); 194 // JsonUtil.skipNextValue(parser); 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 * Get the Java class representing the detected bound object. 215 * 216 * @return the Java class 217 */ 218 @NonNull 219 public Class<? extends IBoundObject> getBoundClass() { 220 return boundClass; 221 } 222 223 /** 224 * Get an {@link InputStream} that can be used to read the analyzed data from 225 * the start. 226 * 227 * @return the stream 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}