blob: f6bd186bd5fc7dcc1e72b58a8dd7c955d146df7a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.uima.json;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.uima.cas.ByteArrayFS;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.Marker;
import org.apache.uima.cas.TypeSystem;
import org.apache.uima.cas.impl.ByteArrayFSImpl;
import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.cas.impl.CasSerializerSupport;
import org.apache.uima.cas.impl.CasSerializerSupport.CasDocSerializer;
import org.apache.uima.cas.impl.CasSerializerSupport.CasSerializerSupportSerialize;
import org.apache.uima.cas.impl.ListUtils;
import org.apache.uima.cas.impl.LowLevelCAS;
import org.apache.uima.cas.impl.MarkerImpl;
import org.apache.uima.cas.impl.TypeImpl;
import org.apache.uima.cas.impl.TypeSystemImpl;
import org.apache.uima.cas.impl.XmiSerializationSharedData;
import org.apache.uima.cas.impl.XmiSerializationSharedData.XmiArrayElement;
import org.apache.uima.internal.util.IntVector;
import org.apache.uima.internal.util.PositiveIntSet;
import org.apache.uima.internal.util.PositiveIntSet_impl;
import org.apache.uima.internal.util.XmlElementName;
import org.apache.uima.internal.util.rb_trees.RedBlackTree;
import org.apache.uima.json.impl.JsonContentHandlerJacksonWrapper;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.io.SerializedString;
/**
* <h2>CAS serializer for JSON formats.</h2>
* <p>Writes a CAS in a JSON format.</p>
*
* <p>To use,</p>
* <ul>
* <li>create an instance of this class,</li>
* <li>(optionally) configure the instance, and then</li>
* <li>call serialize on the instance, optionally passing in additional parameters.</li></ul>
*
* <p>After the 1st 2 steps, the serializer instance may be used for multiple calls (on multiple threads) to
* the 3rd serialize step, if all calls use the same configuration.</p>
*
* <p>There are "convenience" static serialize methods that do these three steps for common configurations.</p>
*
* <p>Parameters can be configured in this instance (I), and/or as part of the serialize(S) call.</p>
*
* <p>The parameters that can be configured are:</p>
* <ul>
* <li>(S) The CAS to serialize
* <li>(S) where to put the output - an OutputStream, Writer, or File</li>
* <li>(I,S) a type system - (default null) if supplied, it is used to "filter" types and features that are serialized. If provided, only
* those that exist in the passed in type system are included in the serialization</li>
* <li>(I,S) a flag for prettyprinting - default false (no prettyprinting)</li>
* </ul>
*
* <p>For Json serialization, additional configuration from the Jackson implementation can be configured</p>
* on 2 associated Jackson instances:
* <ul><li>JsonFactory</li>
* <li>JsonGenerator</li></ul>
* using the standard Jackson methods on the associated JsonFactory instance;
* see the Jackson JsonFactory and JsonGenerator javadocs for details.
*
* <p>These 2 Jackson objects are settable/gettable from an instance of this class.
* They are created if not supplied by the caller.</p>
*
* <p>Once this instance is configured, the serialize method is called
* to serialized a CAS to an output.</p>
*
* <p>Instances of this class must be used on only one thread while configuration is being done;
* afterwards, multiple threads may use the configured instance, to call serialize.</p>
*/
public class JsonCasSerializer {
private static final Integer[] EMPTY_INTEGER_ARRAY = new Integer[0];
private static final SerializedString CONTEXT_NAME = new SerializedString("_context");
private static final SerializedString TYPE_SYSTEM_NAME = new SerializedString("_type_system");
private static final SerializedString TYPES_NAME = new SerializedString("_types");
private static final SerializedString ID_NAME = new SerializedString("_id");
private static final SerializedString SUB_TYPES_NAME = new SerializedString("_subtypes");
private static final SerializedString FEATURE_TYPES_NAME = new SerializedString("_feature_types");
private static final SerializedString FEATURE_REFS_NAME = new SerializedString("_ref");
private static final SerializedString FEATURE_ARRAY_NAME = new SerializedString("_array");
private static final SerializedString FEATURE_BYTE_ARRAY_NAME = new SerializedString("_byte_array");
private static final SerializedString REFERENCED_FSS_NAME = new SerializedString("_referenced_fss");
private static final SerializedString VIEWS_NAME = new SerializedString("_views");
private static final SerializedString TYPE_NAME = new SerializedString("_type");
private static final SerializedString COLLECTION_NAME = new SerializedString("_collection");
private static final SerializedString DELTA_CAS_NAME = new SerializedString("_delta_cas");
private static final SerializedString ADDED_MEMBERS_NAME = new SerializedString("added_members");
private static final SerializedString DELETED_MEMBERS_NAME = new SerializedString("deleted_members");
private static final SerializedString REINDEXED_MEMBERS_NAME = new SerializedString("reindexed_members");
/**
* <p>The serialization can optionally include context information in addition to the feature structures.</p>
*
* <p>This context information is specified, per used-type.</p>
*
* <p>It can be further subdivided into 3 parts:</p>
* <ol>
* <li>What their (used) subtypes are. This enables iterating over a type
* and all of its subtypes, e.g. an iterator over all "Annotations".</li>
* <li> whether or not to include the map from short type names to their fully qualified equivalents.</li>
* <li>Information to enable deserialization of some ambiguous values, depending on the range type of a feature
* </ol>
*
* <p>Some of these may be omitted, if not wanted. This enum allows specifying what to omit.</p>
*
*/
public enum JsonContextFormat {
omitContext, // omit the entire context
omitSubtypes,
omitExpandedTypeNames,
}
private final CasSerializerSupport css = new CasSerializerSupport();
// for testing
CasSerializerSupport getCss() {
return css;
}
private JsonFactory jsonFactory = null;
private boolean isDynamicEmbedding = true;
private boolean isWithContext = true;
private boolean isWithSubtypes = true;
private boolean isWithExpandedTypeNames = true;
private boolean isOmit0Values = false; // https://issues.apache.org/jira/browse/UIMA-4117
private String typeSystemReference;
/***********************************************
* C O N S T R U C T O R S *
***********************************************/
/**
* Creates a new JsonCasSerializer
*/
public JsonCasSerializer() {
}
/**************************************************
* J S O N *
**************************************************/
/****************************************************
* Static JSON Serializer methods for convenience *
* *
* Note: these are named jsonSerialize *
* The non-static methods *
* are named serializeJson *
****************************************************/
/**
* Serializes a CAS using JSON
*
* @param aCAS
* CAS to serialize.
* @param output
* a File, OutputStream or Writer to which to write the XMI document
*
* @throws IOException if there was an IOException
*/
public static void jsonSerialize(CAS aCAS, Object output) throws IOException {
jsonSerialize(aCAS, null, output, false, null, null);
}
/**
* Serializes a CAS to an output (File, OutputStream, XMI stream, or Writer).
* The supplied typesystem filters the output
*
* @param aCAS
* CAS to serialize.
* @param aTargetTypeSystem
* type system used for filtering what gets serialized. Any types or features not in the
* target type system will not be serialized. A null value indicates no filtering, that is,
* that all types and features will be serialized.
* @param output
* output (File, OutputStream, or Writer) to which to write the JSON document
*
* @throws IOException if there was an IOException
*/
public static void jsonSerialize(CAS aCAS, TypeSystem aTargetTypeSystem, Object output)
throws IOException {
jsonSerialize(aCAS, aTargetTypeSystem, output, false, null, null);
}
/**
* Serializes a Delta CAS to an output (File, Writer, or OutputStream).
* This version of this method allows many options to be configured.
*
*
* @param aCAS
* CAS to serialize.
* @param aTargetTypeSystem
* type system to which the produced XMI will conform. Any types or features not in the
* target type system will not be serialized. A null value indicates that all types and features
* will be serialized.
* @param output
* File, Writer, or OutputStream to which to write the JSON document
* @param aPrettyPrint
* if true the JSON output will be formatted with newlines and indenting. If false it will be unformatted.
* @param aMarker
* an optional object used to determine which FeatureStructures and modifications were created after
* the mark was set. Used to serialize a Delta CAS consisting of only new FSs and views and
* preexisting FSs and Views that have been modified. If null, full serialization is done.
* See the JavaDocs for {@link Marker} for details.
* @param sharedData optional, used for delta serialization (not yet supported)
* @throws IOException if there was an IOException
*/
public static void jsonSerialize(CAS aCAS, TypeSystem aTargetTypeSystem, Object output, boolean aPrettyPrint,
Marker aMarker, XmiSerializationSharedData sharedData)
throws IOException {
JsonCasSerializer ser = new JsonCasSerializer();
ser.setFilterTypes((TypeSystemImpl)aTargetTypeSystem);
ser.setPrettyPrint(aPrettyPrint);
ser.serialize(aCAS, output, sharedData, aMarker);
}
/*************************************************************************************
* Multi-step api
*
* 1) Create an instance of this class and use for configuration, specifying or defaulting
* type system to use for filtering (default - no filtering)
* prettyprinting (default - false)
*
* 1b) Do any additional wanted configuration on the instance of this class
* instance.prettyPrint(true/false);
* instance.useJsonFactory(factory)
* instance.filterTypes(typeSystem)
* instance.errorHandler(errorHandler)
* instance.jsonFormat(EnumSet.of(x, y, z)) - default is none of the settings
*
* instance.getGenerator() to further configure the generator if the defaults are not what is wanted.
*
* 2) call its serializeJson method, passing in the CAS, and an output (Writer/Outputstream/File)
*
*************************************************************************************/
/**
* Serialize a Cas to an Output, using configurations set on this instance.
* Constructs a JsonContentHandlerJacksonWrapper, using configured JsonFactory and prettyprint settings if any
* @param cas - the CAS to serialize
* @param output - where the output goes, an OutputStream, Writer, or File
* @throws IOException if there was an IOException
*/
public void serialize(CAS cas, Object output) throws IOException {
serialize(cas, output, null, null);
}
public void serialize(CAS cas, Object output, XmiSerializationSharedData sharedData, Marker marker) throws IOException {
JsonContentHandlerJacksonWrapper jch;
try {
jch = new JsonContentHandlerJacksonWrapper(jsonFactory, output, css.isFormattedOutput);
} catch (SAXException e) {
throw new IOException(e);
}
serialize(cas, jch, sharedData, marker);
}
/**
* Serialize a Cas to an Output configured in the passed in JsonContentHandlerJacksonWrapper
* Constructs a new CasDocSerializer instance to do the serialization,
* configured using this class's Delta marker setting (if any)
* @param cas The CAS to serialize
* @param jch the configured content handler
* @throws IOException if there was an IOException
*/
public void serialize(CAS cas, JsonContentHandlerJacksonWrapper jch) throws IOException {
serialize(cas, jch, null, null);
}
public void serialize(CAS cas, JsonContentHandlerJacksonWrapper jch, XmiSerializationSharedData sharedData, Marker marker) throws IOException {
JsonDocSerializer ser = new JsonDocSerializer(jch, ((CASImpl) cas).getBaseCAS(), sharedData, (MarkerImpl) marker);
try {
ser.cds.needNameSpaces = false;
ser.cds.serialize();
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
}
throw new RuntimeException(e);
}
}
/********************************************************
* Routines to set/reset configuration *
********************************************************/
/**
* set or reset the pretty print flag (default is false)
* @param pp true to do pretty printing of output
* @return the original instance, possibly updated
*/
public JsonCasSerializer setPrettyPrint(boolean pp) {
css.setPrettyPrint(pp);
return this;
}
/**
* set which JsonFactory instance to use; if null, a new instance is used
* this can be used to preconfigure the JsonFactory instance
* @param jsonFactory -
* @return the original instance, possibly updated
*/
public JsonCasSerializer setJsonFactory(JsonFactory jsonFactory) {
this.jsonFactory = jsonFactory;
return this;
}
/**
* pass in a type system to use for filtering what gets serialized;
* only those types and features which are defined this type system are included.
* @param ts the filter
* @return the original instance, possibly updated
*/
public JsonCasSerializer setFilterTypes(TypeSystemImpl ts) {
css.setFilterTypes(ts);
return this;
}
public JsonCasSerializer setTypeSystemReference(String reference) {
typeSystemReference = reference;
return this;
}
// not done here, done on serialize call, because typically changes for each call
// /**
// * set the Marker to specify delta cas serialization
// * forces static embedding mode
// * @param m - the marker
// * @return the original instance, possibly updated
// */
// public JsonCasSerializer setDeltaCas(Marker m, XmiSerializationSharedData sharedData) {
// css.setDeltaCas(m);
// setStaticEmbedding(); // delta requires static embedding mode
// return this;
// }
/**
* set an error handler to receive information about errors
* @param eh the error handler
* @return the original instance, possibly updated
*/
public JsonCasSerializer setErrorHandler(ErrorHandler eh) {
css.setErrorHandler(eh);
return this;
}
/**
* Sets static embedding mode
* @return the original instance, possibly updated
*/
public JsonCasSerializer setStaticEmbedding() {
isDynamicEmbedding = false;
return this;
}
/**
* sets which Json context format to use when serializing
* @param format the format to use for the serialization
* Specifying the context flag also specifies all 3 subflags
* Specifying one of the subflags as true sets the context flag to true if it isn't already
* @return the original instance, possibly updated
*/
public JsonCasSerializer setJsonContext(JsonContextFormat format) {
switch (format) {
case omitContext:
isWithContext = false;
isWithSubtypes = false;
isWithExpandedTypeNames = false; break;
case omitSubtypes:
isWithSubtypes = false; break;
case omitExpandedTypeNames:
isWithExpandedTypeNames = false; break;
}
return this;
}
public JsonCasSerializer setOmit0Values(boolean omitDefaultValues) {
isOmit0Values = omitDefaultValues;
return this;
}
private static class MapType2Subtypes extends RedBlackTree<IntVector> {
/**
*
* @param type main type
* @param subtype subtype of main type
* @return true if added, false if already was there
*/
boolean addSubtype(int type, int subtype) {
IntVector iv = get(type);
if (null == iv) {
iv = new IntVector();
iv.add(subtype);
put(type, iv);
return true;
}
if (iv.contains(subtype)) {
return false;
}
iv.add(subtype);
return true;
}
}
class JsonDocSerializer extends CasSerializerSupportSerialize {
private final CasDocSerializer cds;
private final JsonContentHandlerJacksonWrapper jch;
private final JsonGenerator jg;
private final String typeSystemReference;
private final Map<String, SerializedString> serializedStrings = new HashMap<String, SerializedString>();
private final Map<String, XmlElementName> usedTypeName2XmlElementName;
private final MapType2Subtypes mapType2Subtypes = new MapType2Subtypes();
private final IntVector parentTypesWithNoInstances = new IntVector();
private int lastEncodedTypeCode;
private boolean startedReferencedFSs;
private final boolean isOmitDefaultValues;
private final boolean isWithContext;
private final boolean isWithExpandedTypeNames;
private final boolean isWithSubtypes;
private boolean indexId; // true causes fs to be listed as "id" : { ...}, false as "type" : [ {...}
private boolean isEmbedded = false; // true for embedded FSs, causes _type to be included
private boolean isEmbeddedFromFsFeature; // used for NL formatting, false if embedded due to Array or List
private boolean startedFeatureTypes;
private JsonDocSerializer(ContentHandler ch, CASImpl cas, XmiSerializationSharedData sharedData, MarkerImpl marker) {
cds = css.new CasDocSerializer(ch, cas, sharedData, marker, this, JsonCasSerializer.this.isDynamicEmbedding);
this.isOmitDefaultValues = JsonCasSerializer.this.isOmit0Values;
isWithExpandedTypeNames = JsonCasSerializer.this.isWithExpandedTypeNames;
isWithSubtypes = JsonCasSerializer.this.isWithSubtypes;
typeSystemReference = JsonCasSerializer.this.typeSystemReference;
jch = (JsonContentHandlerJacksonWrapper) ch;
jg = jch.getJsonGenerator();
isWithContext = JsonCasSerializer.this.isWithContext || isWithSubtypes || isWithExpandedTypeNames;
usedTypeName2XmlElementName = new HashMap<String, XmlElementName>(cds.tsi.getNumberOfTypes());
}
@Override
protected void initializeNamespaces() {
if (cds.sharedData != null &&
(null != cds.sharedData.getOutOfTypeSystemElements() ||
cds.sharedData.hasOutOfTypeSystemArrayElements())) {
throw new UnsupportedOperationException("Can't do JSON serialization "
+ "if there are out-of-type-system elements,"
+ " because there's no type information available (needed for _context)");
}
}
@Override
protected void writeViews() throws Exception {
if (!cds.isDelta) {
return;
}
jch.writeNlJustBeforeNext();
jg.writeFieldName(DELTA_CAS_NAME);
jg.writeStartObject();
cds.writeViewsCommons(); // encodes cas.sofaCount + 1 elements
jg.writeEndObject(); // and end of views property
}
@Override
protected void writeFeatureStructures(int elementCount /* not used */ ) throws Exception{
jch.withoutNl(); // set up prettyprint mode so this class controls it
jg.writeStartObject(); // container for (maybe) context, fss (2 parts), and (maybe) delta view info
if (isWithContext) {
serializeJsonLdContext();
}
jch.writeNlJustBeforeNext();
// write the reachable from indexes FS
indexId = false;
jg.writeFieldName(VIEWS_NAME);
jg.writeStartObject();
final Integer[][] byViewByTypeFSs = sortByViewType();
for (int viewNbr = 1; viewNbr <= byViewByTypeFSs.length; viewNbr++) {
// viewNbr starts at 1
lastEncodedTypeCode = -1;
final Integer[] fssInView = byViewByTypeFSs[viewNbr - 1];
final int sofaAddr = cds.getSofaAddr(viewNbr);
if (sofaAddr == 0 && fssInView.length == 0) {
continue; // skip non-existent initial view with no sofa and no elements
}
jch.writeNlJustBeforeNext();
String viewName = (0 == sofaAddr) ?
CAS.NAME_DEFAULT_SOFA :
cds.cas.getStringValue(sofaAddr, TypeSystemImpl.sofaIdFeatCode);
jg.writeFieldName(viewName); // view namne
jg.writeStartObject();
for (Integer fs : fssInView) {
cds.encodeFS(fs);
}
if (lastEncodedTypeCode != -1) {
jg.writeEndArray(); // of array of types under a fs
}
jg.writeEndObject();
}
jg.writeEndObject(); // end of value for _views
// write the non-embeddable referenced FSs
indexId = true;
startedReferencedFSs = false;
cds.encodeQueued();
if (startedReferencedFSs) {
jg.writeEndObject(); // of all referenced FSs
}
}
@Override
protected void writeEndOfSerialization() throws IOException {
jg.writeEndObject(); // wrapper of _context and cas
jg.flush();
}
// sort the by-view by-type set
// previously Serialized
private Integer[][] sortByViewType() {
final Integer[] [] r = new Integer[cds.indexedFSs.length] [];
int i = 0;
for (final IntVector fss : cds.indexedFSs) {
final Integer[] viewFSs = r[i++] = (null == fss) ? EMPTY_INTEGER_ARRAY : new Integer[fss.size()];
for (int j = 0; j < viewFSs.length; j++) {
viewFSs[j] = fss.get(j);
}
Arrays.sort(viewFSs, cds.sortFssByType);
}
return r;
}
@Override
protected void writeView(int sofaAddr, int[] members) throws IOException {
jch.writeNlJustBeforeNext();
String sofaXmiId = (0 == sofaAddr) ? "0" : cds.getXmiId(sofaAddr);
jg.writeArrayFieldStart(sofaXmiId);
writeViewMembers(members);
//check for out-of-typesystem members
if (cds.sharedData != null) {
List<String> ootsMembers = cds.sharedData.getOutOfTypeSystemViewMembers(sofaXmiId);
jch.writeNlJustBeforeNext();
writeViewMembers(ootsMembers);
}
jg.writeEndArray();
}
private void writeViewForDeltas(SerializedString kind, int[] deltaMembers) throws IOException {
jg.writeFieldName(kind);
jg.writeStartArray();
writeViewMembers(deltaMembers);
jg.writeEndArray();
}
@Override
protected void writeView(int sofaAddr, int[] added, int[] deleted, int[] reindexed) throws IOException {
jch.writeNlJustBeforeNext();
jg.writeFieldName(cds.getXmiId(sofaAddr));
jg.writeStartObject();
writeViewForDeltas(ADDED_MEMBERS_NAME, added);
writeViewForDeltas(DELETED_MEMBERS_NAME, deleted);
writeViewForDeltas(REINDEXED_MEMBERS_NAME, reindexed);
jg.writeEndObject();
}
private void writeViewMembers(int[] members) throws IOException {
int nextBreak = CasSerializerSupport.PP_ELEMENTS;
int i = 0;
for (int member : members) {
int xmiId = cds.getXmiIdAsInt(member);
if (xmiId == 0) {
continue;
}
if (i++ > nextBreak) {
jch.writeNlJustBeforeNext();
nextBreak += CasSerializerSupport.PP_ELEMENTS;
}
jg.writeNumber(xmiId);
}
}
/*
* version for oots data
*/
private void writeViewMembers(List<String> members) throws IOException {
int nextBreak = CasSerializerSupport.PP_ELEMENTS;
int i = 0;
for (String xmiId : members) {
if (null == xmiId || xmiId.length() == 0) {
continue;
}
if (i++ > nextBreak) {
jch.writeNlJustBeforeNext();
nextBreak += CasSerializerSupport.PP_ELEMENTS;
}
jg.writeNumber(Integer.parseInt(xmiId));
}
}
/**
* <h2>JSON: serialize context info</h2>
*
* <p>The context has several parts.
* <p>The typeSystemReference is an optional URI to a type system that is written out.
* <p>The types part is organized by the type hierarchy, starting with the uima.cas.TOP type. There is an entry
* for each type which has 1 or more serailized instances, and also for all supertypes of those types.
* The entry is a JSON key-value pair "short-type-name" : {...}. </p>
*
* <p>The information for each type has 3 sections:</p>
* <ol>
* <li>_subtypes - a JSON map of key-value pairs, keyed by the short type-name of
* used subtypes of this type. If this type has
* no used subtypes, this element is omitted.
* The value is an instance of this structure, for that type.</li>
*
* <li>_id - the fully qualified UIMA type name</li>
*
* <li>@featureTypes - a map with keys being specific features of the type
* that need extra information about their contents,
* and the value being that extra information.</li>
* </ol>
*
* RANGE_IDs specify the type of the value of a feature. There are currently 2 kinds:
*
* <ul>
* <li>"@featureByteArray" - indicates the string value should be decoded as a base64 binary encoded byte array</li>
* <li>"{ "@featureRef" : "short_type_name" } - indicates the number or array of numbers
* should be interpreted as a reference to a FS having this number (or array of numbers)
* as its id(s).
* 0 is interpreted as a null reference.
* The type of the FS being referred to is of type "short_type_name" or a subtype.</li>
* </ul>
* @throws IOException
*/
private void serializeJsonLdContext() throws IOException {
jg.writeFieldName(CONTEXT_NAME);
jg.writeStartObject();
if (typeSystemReference != null) {
jch.writeNlJustBeforeNext();
jg.writeFieldName(TYPE_SYSTEM_NAME);
jg.writeString(typeSystemReference);
}
collectUsedSubtypes();
jch.writeNlJustBeforeNext();
jg.writeFieldName(TYPES_NAME);
jg.writeStartObject();
for (TypeImpl ti : cds.getSortedUsedTypes()) {
jch.writeNlJustBeforeNext();
jg.writeFieldName(getSerializedTypeName(ti.getCode()));
jg.writeStartObject();
if (isWithExpandedTypeNames) {
jg.writeFieldName(ID_NAME); // form for using SerializedString
jg.writeString(ti.getName());
}
addJsonFeatContext(ti);
if (isWithSubtypes) {
addJsonSubtypes(ti);
}
jg.writeEndObject(); // end of one type
}
// write out contexts for types in the supertype chain which have no instances
for (final int typeCode : parentTypesWithNoInstances.toArray()) {
jch.writeNlJustBeforeNext();
jg.writeFieldName(getSerializedTypeName(typeCode));
jg.writeStartObject();
XmlElementName xe = cds.typeCode2namespaceNames[typeCode];
if (isWithExpandedTypeNames) {
jg.writeFieldName(ID_NAME); // form for using SerializedString
jg.writeString(xe.nsUri);
}
addJsonFeatContext(typeCode);
if (isWithSubtypes) {
addJsonSubtypes(typeCode);
}
jg.writeEndObject(); // end of one type
}
jg.writeEndObject(); // end of _types
jg.writeEndObject(); // end of _context
}
/**
* _feature_types : { "featName" : "_ref" or "_byte_array, ... }
*
* @param type the type for which to generate the feature context info
* @throws IOException
*/
private void addJsonFeatContext(TypeImpl type) throws IOException {
addJsonFeatContext(type.getCode());
}
private void addJsonFeatContext(final int typeCode) throws IOException {
final int[] feats = cds.tsi.ll_getAppropriateFeatures(typeCode);
startedFeatureTypes = false;
for (int featCode : feats) {
final int fsClass = cds.classifyType(cds.tsi.range(featCode));
SerializedString featKind = featureTypeLabel(fsClass, featCode);
if (null != featKind) {
maybeDoStartFeatureTypes();
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeString(featKind);
}
}
if (startedFeatureTypes) {
jg.writeEndObject();
}
}
private void maybeDoStartFeatureTypes() throws IOException {
if (!startedFeatureTypes) {
jch.writeNlJustBeforeNext();
jg.writeFieldName(FEATURE_TYPES_NAME);
jg.writeStartObject();
startedFeatureTypes = true;
}
}
private SerializedString getShortFeatureName(int featCode) {
return getSerializedString(cds.tsi.ll_getFeatureForCode(featCode).getShortName());
}
/**
* Add subtype information for used types limited to used subtypes
* @throws IOException
*/
private void addJsonSubtypes(TypeImpl ti) throws IOException {
addJsonSubtypes(ti.getCode());
}
private void addJsonSubtypes(int aTypeCode) throws IOException {
IntVector iv = mapType2Subtypes.get(aTypeCode);
if (null != iv && iv.size() > 0) {
jch.writeNlJustBeforeNext();
jg.writeFieldName(SUB_TYPES_NAME);
jg.writeStartArray();
for (int typeCode : iv.toArray()) {
jg.writeString(getSerializedTypeName(typeCode));
}
jg.writeEndArray();
}
}
private void collectUsedSubtypes() {
final TypeImpl[] tiArray = cds.getSortedUsedTypes();
for (TypeImpl ti : tiArray) { // all used types
int subtypeCode = ti.getCode();
// loop up the super chain for this type,
// add parent -> subtype entries (until try to add one that's already there)
for (TypeImpl parent = (TypeImpl) ti.getSuperType();
parent != null;
parent = (TypeImpl) parent.getSuperType()) {
final int parentCode = parent.getCode();
// next comparator must match the one used for sorting the tiArray
// https://issues.apache.org/jira/browse/UIMA-5171
// if parent not contained in tiArray
if (Arrays.binarySearch(tiArray, parent, CasSerializerSupport.COMPARATOR_SHORT_TYPENAME) < 0 ) {
if (!parentTypesWithNoInstances.contains(parentCode)) {
parentTypesWithNoInstances.add(parentCode);
}
}
boolean wasAdded = mapType2Subtypes.addSubtype(parentCode, subtypeCode);
if (!wasAdded) {
break;
}
subtypeCode = parentCode;
}
}
}
private SerializedString getSerializedTypeName(int typeCode) {
XmlElementName xe = cds.typeCode2namespaceNames[typeCode];
if (null == xe) {
// happens for supertypes which have no instantiations
String typeName = cds.tsi.ll_getTypeForCode(typeCode).getName();
xe = uimaTypeName2XmiElementName(typeName);
checkForNameCollision(xe);
cds.typeCode2namespaceNames[typeCode] = xe;
}
return getSerializedString(xe.qName);
}
private SerializedString getSerializedString(String s) {
SerializedString ss = serializedStrings.get(s);
if (ss == null) {
ss = new SerializedString(s);
serializedStrings.put(s, ss);
}
return ss;
}
/*
* keep map from short type name to XmlElementName (full name, namespace, etc)
* This map starts out empty
* first use of type puts entry in
* first use of type with different full name adds namespace to both
*/
@Override
protected void checkForNameCollision(XmlElementName xmlElementName) {
XmlElementName xel = usedTypeName2XmlElementName.get(xmlElementName.localName);
if (xel != null) {
if (xel.nsUri.equals(xmlElementName.nsUri)) { // nsUri is the fully qualified name
return; // don't need name spaces yet, or have already added them for this item
} else {
addNameSpace(xel);
addNameSpace(xmlElementName);
// usedTypeName2XmlElementName.clear(); // not needed anymore
return;
}
}
usedTypeName2XmlElementName.put(xmlElementName.localName, xmlElementName);
return;
}
@Override
protected boolean writeFsStart(int addr, int typeCode) throws IOException {
if (isEmbedded) {
if (!isEmbeddedFromFsFeature) {
jch.writeNlJustBeforeNext(); // if from feature, already did nl
}
jg.writeStartObject();
} else if (indexId) {
if (!startedReferencedFSs) {
jch.writeNlJustBeforeNext();
jg.writeFieldName(REFERENCED_FSS_NAME);
jg.writeStartObject();
startedReferencedFSs = true;
}
jch.writeNlJustBeforeNext();
jg.writeFieldName(cds.getXmiId(addr));
jg.writeStartObject(); // start of feat : value
} else { // fs's as arrays under typeName
if (typeCode != lastEncodedTypeCode) {
if (lastEncodedTypeCode != -1) {
// close off previous Array
jg.writeEndArray();
}
lastEncodedTypeCode = typeCode;
jch.writeNlJustBeforeNext();
jg.writeFieldName(getSerializedTypeName(typeCode));
jg.writeStartArray();
}
// if we're not going to write the actual FS here,
// and are just going to write the ref,
// skip the start object
if (!cds.isDynamicMultiRef || !cds.multiRefFSs.contains(addr)) {
jch.writeNlJustBeforeNext();
jg.writeStartObject(); // start of feat : value
}
}
return indexId;
}
@Override
protected void writeFsRef(int addr) throws Exception {
jg.writeNumber(cds.getXmiIdAsInt(addr));
}
// private void maybeWriteIdFeat(int addr) throws IOException {
// if (!omitId) {
// jg.writeFieldName(ID_NAME);
// jg.writeNumber(cds.getXmiIdAsInt(addr));
// }
// }
private void maybeWriteTypeFeat(int typeCode) throws IOException {
if (indexId || isEmbedded) {
jg.writeFieldName(TYPE_NAME);
jg.writeString(getSerializedTypeName(typeCode));
}
}
@Override
protected void writeFs(int addr, int typeCode) throws IOException {
writeFsOrLists(addr, typeCode, false);
}
@Override
protected void writeListsAsIndividualFSs(int addr, int typeCode) throws IOException {
writeFsOrLists(addr, typeCode, true);
}
private void writeFsOrLists(int addr, int typeCode, boolean isListAsFSs) throws IOException {
final int[] feats = cds.tsi.ll_getAppropriateFeatures(typeCode);
// maybeWriteIdFeat(addr);
maybeWriteTypeFeat(typeCode);
for (final int featCode : feats) {
if (cds.isFiltering) {
// skip features that aren't in the target type system
String fullFeatName = cds.tsi.ll_getFeatureForCode(featCode).getName();
if (cds.filterTypeSystem.getFeatureByFullName(fullFeatName) == null) {
continue;
}
}
final int featAddr = addr + cds.cas.getFeatureOffset(featCode);
final int featValRaw = cds.cas.getHeapValue(featAddr);
final int featureClass = cds.classifyType(cds.tsi.range(featCode));
switch (featureClass) {
case LowLevelCAS.TYPE_CLASS_BYTE:
case LowLevelCAS.TYPE_CLASS_SHORT:
case LowLevelCAS.TYPE_CLASS_INT:
if (featValRaw == 0 && isOmitDefaultValues) continue;
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeNumber(featValRaw);
break;
case LowLevelCAS.TYPE_CLASS_FS:
if (featValRaw == 0/* && isOmitDefaultValues*/) continue;
writeFsOrRef(featValRaw, featCode); // writes nl before embedded fs
break;
case LowLevelCAS.TYPE_CLASS_LONG:
final long longVal = cds.cas.ll_getLongValue(featValRaw);
if (longVal == 0L && isOmitDefaultValues) continue;
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeNumber(longVal);
break;
case LowLevelCAS.TYPE_CLASS_FLOAT:
final float floatVal = CASImpl.int2float(featValRaw);
if (floatVal == 0.F && isOmitDefaultValues) continue;
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeNumber(floatVal);
break;
case LowLevelCAS.TYPE_CLASS_DOUBLE:
final double doubleVal = cds.cas.ll_getDoubleValue(addr, featCode);
if (doubleVal == 0L && isOmitDefaultValues) continue;
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeNumber(doubleVal);
break;
case LowLevelCAS.TYPE_CLASS_BOOLEAN:
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeBoolean(cds.cas.ll_getBooleanValue(addr, featCode));
break;
case LowLevelCAS.TYPE_CLASS_STRING:
if (featValRaw == 0 /*&& isOmitDefaultValues*/) continue;
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeString(cds.cas.getStringForCode(featValRaw));
break;
// all other fields (arrays, lists, fsRefs) can be null and are omitted if so
default:
if (featValRaw != CASImpl.NULL /*|| !isOmitDefaultValues*/) {
jg.writeFieldName(getShortFeatureName(featCode));
if (featureClass == LowLevelCAS.TYPE_CLASS_INTARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_FLOATARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_BOOLEANARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_BYTEARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_SHORTARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_LONGARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_DOUBLEARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_STRINGARRAY ||
featureClass == LowLevelCAS.TYPE_CLASS_FSARRAY) {
if (isDynamicOrStaticMultiRef(featCode, featValRaw)) {
jg.writeNumber(cds.getXmiIdAsInt(featValRaw));
} else {
writeJsonArrayValues(featValRaw, featureClass);
}
} else if (featureClass == CasSerializerSupport.TYPE_CLASS_INTLIST ||
featureClass == CasSerializerSupport.TYPE_CLASS_FLOATLIST ||
featureClass == CasSerializerSupport.TYPE_CLASS_STRINGLIST ||
featureClass == CasSerializerSupport.TYPE_CLASS_FSLIST) {
if (isDynamicOrStaticMultiRef(featCode, featValRaw, isListAsFSs)) {
jg.writeNumber(cds.getXmiIdAsInt(featValRaw));
} else {
writeJsonListValues(featValRaw);
}
} else { // is error
throw new RuntimeException("Invalid State, featureClass was "+ featureClass);
}
} // end of default case with non-null values
} // end of switch
} // end of loop over all features
}
/**
* for arrays and lists,
* recursively write one FS,
* as actual FS,
* if dynamic embedding and single ref
* OR, just write the reference id
* If trying to write the null FS (due to filtering for instance), write 0
* @param addr
* @throws IOException
*/
private void writeFsOrRef(int addr) throws IOException {
if (addr == 0 || !cds.isDynamicMultiRef || cds.multiRefFSs.contains(addr)) {
jg.writeNumber(cds.getXmiIdAsInt(addr));
} else {
isEmbeddedFromFsFeature = false;
writeEmbeddedFs(addr);
}
}
private void writeEmbeddedFs(int addr) throws IOException {
boolean savedEmbedded = isEmbedded;
try {
isEmbedded = true;
cds.encodeFS(addr);
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
}
throw new RuntimeException(e);
} finally {
isEmbedded = savedEmbedded;
} // embed
}
private void writeFsOrRef(int addr, int featCode) throws IOException {
if (addr == 0 || !cds.isDynamicMultiRef || cds.multiRefFSs.contains(addr)) {
jg.writeFieldName(getShortFeatureName(featCode));
jg.writeNumber(cds.getXmiIdAsInt(addr));
} else {
jch.writeNlJustBeforeNext();
jg.writeFieldName(getShortFeatureName(featCode));
isEmbeddedFromFsFeature = true;
// Use cases: can write embed, which has embed, which has non-embed
// once hit non-embed, this flag would be turned off,
// But it's only tested at the beginning of writeEmbeddedFs, so subsequent fields reset this
// This flag only used to control new lines for embedded case
writeEmbeddedFs(addr);
isEmbeddedFromFsFeature = false; // restore default
}
}
/**
* Write FSArrays
*/
@Override
protected void writeArrays(int addr, int typeCode, int typeClass) throws IOException {
// maybeWriteIdFeat(addr);
maybeWriteTypeFeat(typeCode);
jg.writeFieldName(COLLECTION_NAME);
writeJsonArrayValues(addr, typeClass);
}
@Override
protected void writeEndOfIndividualFs() throws IOException {
jg.writeEndObject();
}
// writes a set of values in a JSON array
// or null if the reference to the UIMA array is actually null
// 0 length arrays are written as []
// Note: FSs can be embedded for FS Arrays
private void writeJsonArrayValues(int addr, int arrayType) throws IOException {
if (addr == CASImpl.NULL) {
jg.writeNull();
return;
}
cds.visited_not_yet_written.remove(addr);
final int array_size = cds.cas.ll_getArraySize(addr);
if (arrayType == LowLevelCAS.TYPE_CLASS_BYTEARRAY) {
// special case for byte arrays:
// serialize using standard JACKSON/JSON binary serialization
// (doing extra copy to avoid figuring out the impl details)
ByteArrayFS byteArrayFS = new ByteArrayFSImpl(addr, cds.cas);
int length = byteArrayFS.size();
byte[] byteArray = new byte[length];
byteArrayFS.copyToArray(0, byteArray, 0, length);
jg.writeBinary(byteArray);
} else {
jg.writeStartArray();
int pos = cds.cas.getArrayStartAddress(addr);
if (arrayType == LowLevelCAS.TYPE_CLASS_FSARRAY) {
List<XmiArrayElement> ootsArrayElementsList = cds.sharedData == null ? null :
cds.sharedData.getOutOfTypeSystemArrayElements(addr);
int ootsIndex = 0;
for (int j = 0; j < array_size; j++) { // j used to id the oots things
int heapValue = cds.cas.getHeapValue(pos++);
if (heapValue == CASImpl.NULL) {
// this null array element might have been a reference to an
// out-of-typesystem FS, which, when deserialized, was replaced with NULL,
// so check the ootsArrayElementsList
boolean found = false;
if (ootsArrayElementsList != null) {
while (ootsIndex < ootsArrayElementsList.size()) {
XmiArrayElement arel = ootsArrayElementsList.get(ootsIndex++);
if (arel.index == j) {
jg.writeNumber(Integer.parseInt(arel.xmiId));
found = true;
break;
}
}
}
if (!found) {
jg.writeNumber(0);
}
// else, not null FS ref
} else {
if (cds.isFiltering) { // return as null any references to types not in target TS
String typeName = cds.tsi.ll_getTypeForCode(cds.cas.getHeapValue(addr)).getName();
if (cds.filterTypeSystem.getType(typeName) == null) {
heapValue = CASImpl.NULL;
}
}
writeFsOrRef(heapValue); // allow embedding in array
}
} // end of loop over all refs in FS array
} else {
for (int i = 0; i < array_size; i++) {
if (arrayType == LowLevelCAS.TYPE_CLASS_BOOLEANARRAY) {
jg.writeBoolean(cds.cas.ll_getBooleanArrayValue(addr, i));
} else if (arrayType == LowLevelCAS.TYPE_CLASS_STRINGARRAY) {
jg.writeString(cds.cas.ll_getStringArrayValue(addr, i));
} else if (arrayType == LowLevelCAS.TYPE_CLASS_SHORTARRAY) {
jg.writeNumber(cds.cas.ll_getShortArrayValue(addr, i));
} else if (arrayType == LowLevelCAS.TYPE_CLASS_INTARRAY) {
jg.writeNumber(cds.cas.ll_getIntArrayValue(addr, i));
} else if (arrayType == LowLevelCAS.TYPE_CLASS_LONGARRAY) {
jg.writeNumber(cds.cas.ll_getLongArrayValue(addr, i));
} else if (arrayType == LowLevelCAS.TYPE_CLASS_FLOATARRAY) {
jg.writeNumber(cds.cas.ll_getFloatArrayValue(addr, i));
} else {
jg.writeNumber(cds.cas.ll_getDoubleArrayValue(addr, i));
}
}
}
jg.writeEndArray();
}
}
// a null ref is written as null
// an empty list is written as []
/**
* Only called if no sharing of list nodes exists (except for non-dynamic case)
* Only called for list nodes referred to by Feature value slots in some FS.
* @param curNode the address of the start of the list
* @throws IOException
*/
private void writeJsonListValues(int curNode) throws IOException {
if (curNode == CASImpl.NULL) {
throw new RuntimeException("never happen");
}
final ListUtils listUtils = cds.listUtils;
final int startNodeType = cds.cas.getHeapValue(curNode);
int headFeat = listUtils.getHeadFeatCode(startNodeType);
int tailFeat = listUtils.getTailFeatCode(startNodeType);
int neListType = listUtils.getNeListType(startNodeType); // non-empty
final PositiveIntSet visited = new PositiveIntSet_impl();
jg.writeStartArray();
while (curNode != CASImpl.NULL) {
cds.visited_not_yet_written.remove(curNode);
final int curNodeType = cds.cas.getHeapValue(curNode);
if (curNodeType != neListType) { // if not "non-empty"
break; // would be the end element. a 0 is also treated as an end element
}
if (!visited.add(curNode)) {
break; // loop detected, stop. no error report here, would be reported earlier during enqueue
}
final int val = cds.cas.getHeapValue(curNode + cds.cas.getFeatureOffset(headFeat));
if (curNodeType == listUtils.neStringListType) {
jg.writeString(cds.cas.getStringForCode(val));
} else if (curNodeType == listUtils.neFloatListType) {
jg.writeNumber(CASImpl.int2float(val));
} else if (curNodeType == listUtils.neFsListType) {
writeFsOrRef(val); // maybe embed
} else { // for ints
jg.writeNumber(val);
}
curNode = cds.cas.getHeapValue(curNode + cds.cas.getFeatureOffset(tailFeat));
}
jg.writeEndArray();
}
/**
* Return null or a string representing the type of the feature
*
*
* @param fsClass the class of the feature
* @param featCode the feature code
* @return _ref, _array, _byte_array, or null
*/
private SerializedString featureTypeLabel(int fsClass, int featCode) {
switch (fsClass) {
case LowLevelCAS.TYPE_CLASS_FS:
case LowLevelCAS.TYPE_CLASS_FSARRAY:
case CasSerializerSupport.TYPE_CLASS_FSLIST:
return FEATURE_REFS_NAME;
case LowLevelCAS.TYPE_CLASS_INTARRAY:
case LowLevelCAS.TYPE_CLASS_FLOATARRAY:
case LowLevelCAS.TYPE_CLASS_STRINGARRAY:
case LowLevelCAS.TYPE_CLASS_BOOLEANARRAY:
case LowLevelCAS.TYPE_CLASS_SHORTARRAY:
case LowLevelCAS.TYPE_CLASS_LONGARRAY:
case LowLevelCAS.TYPE_CLASS_DOUBLEARRAY:
case CasSerializerSupport.TYPE_CLASS_INTLIST:
case CasSerializerSupport.TYPE_CLASS_FLOATLIST:
case CasSerializerSupport.TYPE_CLASS_STRINGLIST:
// we have refs only if the feature has
// multipleReferencesAllowed = true
return FEATURE_ARRAY_NAME;
case LowLevelCAS.TYPE_CLASS_BYTEARRAY:
return FEATURE_BYTE_ARRAY_NAME;
default: // for primitives
return null;
}
}
/**
* Converts a UIMA-style dotted type name to the element name that should be used in the
* serialization. The XMI element name consists of three parts - the Namespace URI, the Local
* Name, and the QName (qualified name).
*
* @param uimaTypeName
* a UIMA-style dotted type name
* @return a data structure holding the three components of the XML element name
*/
@Override
protected XmlElementName uimaTypeName2XmiElementName(String uimaTypeName) {
// split uima type name into namespace and short name
String shortName;
final int lastDotIndex = uimaTypeName.lastIndexOf('.');
if (lastDotIndex == -1) { // no namespace
shortName = uimaTypeName;
} else {
shortName = uimaTypeName.substring(lastDotIndex + 1);
}
// convert short name to shared string, without interning, reduce GCs
shortName = cds.getUniqueString(shortName);
return new XmlElementName(uimaTypeName, shortName, shortName); // use short name for qname until namespaces needed
}
/**
* Called to generate a new namespace prefix and add it to this element - due to a collision
* @param xmlElementName
*/
@Override
protected void addNameSpace(XmlElementName xmlElementName) {
if (xmlElementName.qName.equals(xmlElementName.localName)) { // may have already had namespace added
// split uima type name into namespace and short name
String uimaTypeName = xmlElementName.nsUri;
String shortName = xmlElementName.localName;
final int lastDotIndex = uimaTypeName.lastIndexOf('.');
// determine what namespace prefix to use
String prefix = cds.getNameSpacePrefix(uimaTypeName, uimaTypeName, lastDotIndex);
xmlElementName.qName = cds.getUniqueString(prefix + ':' + shortName);
}
}
private boolean isDynamicOrStaticMultiRef(int featCode, int addr) {
return (!cds.isDynamicMultiRef) ?
cds.isStaticMultiRef(featCode) :
cds.multiRefFSs.contains(addr);
}
private boolean isDynamicOrStaticMultiRef(int featCode, int addr, boolean isListAsFSs) {
return (!cds.isDynamicMultiRef) ?
(isListAsFSs || cds.isStaticMultiRef(featCode)) :
cds.multiRefFSs.contains(addr);
}
}
}