Commit 28991c52 by Shwetha GS

ATLAS-645 FieldMapping.output() results in stack overflow when instances…

ATLAS-645 FieldMapping.output() results in stack overflow when instances reference each other (dkantor via shwethags)
parent 454feb47
......@@ -20,6 +20,7 @@ ATLAS-409 Atlas will not import avro tables with schema read from a file (dosset
ATLAS-379 Create sqoop and falcon metadata addons (venkatnrangan,bvellanki,sowmyaramesh via shwethags)
ALL CHANGES:
ATLAS-645 FieldMapping.output() results in stack overflow when instances reference each other (dkantor via shwethags)
ATLAS-733 UI: "undefined" XHR request is made for every entity GET page request. (kevalbhatt18 via yhemanth)
ATLAS-663,ATLAS-673 Install Setup: SOLR (tbeerbower via sumasai)
ATLAS-629 Kafka messages in ATLAS_HOOK might be lost in HA mode at the instant of failover. (yhemanth)
......
......@@ -20,7 +20,9 @@ package org.apache.atlas.typesystem.persistence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IReferenceableInstance;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.ITypedReferenceableInstance;
import org.apache.atlas.typesystem.ITypedStruct;
......@@ -33,6 +35,7 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Date;
import java.util.HashSet;
/*
* @todo handle names prefixed by traitName.
......@@ -89,7 +92,7 @@ public class ReferenceableInstance extends StructInstance implements ITypedRefer
StringBuilder buf = new StringBuilder();
String prefix = "";
fieldMapping.output(this, buf, prefix);
fieldMapping.output(this, buf, prefix, new HashSet<IReferenceableInstance>());
return buf.toString();
} catch (AtlasException me) {
......
......@@ -20,8 +20,8 @@ package org.apache.atlas.typesystem.persistence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.ITypedStruct;
import org.apache.atlas.typesystem.types.AttributeInfo;
import org.apache.atlas.typesystem.types.ClassType;
......@@ -31,7 +31,6 @@ import org.apache.atlas.typesystem.types.EnumValue;
import org.apache.atlas.typesystem.types.FieldMapping;
import org.apache.atlas.typesystem.types.StructType;
import org.apache.atlas.typesystem.types.TypeSystem;
import org.apache.atlas.typesystem.types.TypeUtils;
import org.apache.atlas.typesystem.types.ValueConversionException;
import org.apache.atlas.utils.MD5Utils;
......@@ -724,32 +723,13 @@ public class StructInstance implements ITypedStruct {
strings[pos] = val;
}
public void output(IStruct s, Appendable buf, String prefix) throws AtlasException {
TypeUtils.outputVal("{", buf, prefix);
if (s == null) {
TypeUtils.outputVal("<null>\n", buf, "");
return;
}
TypeUtils.outputVal("\n", buf, "");
String fieldPrefix = prefix + "\t";
for (Map.Entry<String, AttributeInfo> e : fieldMapping.fields.entrySet()) {
String attrName = e.getKey();
AttributeInfo i = e.getValue();
Object aVal = s.get(attrName);
TypeUtils.outputVal(attrName + " : ", buf, fieldPrefix);
i.dataType().output(aVal, buf, "");
TypeUtils.outputVal("\n", buf, "");
}
TypeUtils.outputVal("\n}\n", buf, "");
}
@Override
public String toString() {
try {
StringBuilder buf = new StringBuilder();
String prefix = "";
fieldMapping.output(this, buf, prefix);
fieldMapping.output(this, buf, prefix, null);
return buf.toString();
} catch (AtlasException me) {
......
......@@ -22,7 +22,9 @@ import com.google.common.collect.ImmutableSortedMap;
import org.apache.atlas.AtlasException;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
abstract class AbstractDataType<T> implements IDataType<T> {
......@@ -44,7 +46,7 @@ abstract class AbstractDataType<T> implements IDataType<T> {
}
@Override
public void output(T val, Appendable buf, String prefix) throws AtlasException {
public void output(T val, Appendable buf, String prefix, Set<T> inProcess) throws AtlasException {
if (val instanceof Map) {
ImmutableSortedMap immutableSortedMap = ImmutableSortedMap.copyOf((Map) val);
TypeUtils.outputVal(val == null ? "<null>" : immutableSortedMap.toString(), buf, prefix);
......@@ -53,6 +55,24 @@ abstract class AbstractDataType<T> implements IDataType<T> {
}
}
@Override
public void output(Appendable buf, Set<String> typesInProcess) throws AtlasException {
try {
buf.append(toString());
} catch (IOException e) {
throw new AtlasException(e);
}
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "{name=" + name + ", description=" + description + "}";
}
/**
* Validate that current definition can be updated with the new definition
* @param newType
......
......@@ -22,7 +22,10 @@ import org.apache.atlas.AtlasException;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class AttributeInfo {
public final String name;
......@@ -60,15 +63,30 @@ public class AttributeInfo {
@Override
public String toString() {
return "AttributeInfo{" +
"name='" + name + '\'' +
", dataType=" + dataType +
", multiplicity=" + multiplicity +
", isComposite=" + isComposite +
", isUnique=" + isUnique +
", isIndexable=" + isIndexable +
", reverseAttributeName='" + reverseAttributeName + '\'' +
'}';
StringBuilder buf = new StringBuilder();
try {
output(buf, new HashSet<String>());
} catch (AtlasException e) {
throw new RuntimeException(e);
}
return buf.toString();
}
public void output(Appendable buf, Set<String> typesInProcess) throws AtlasException {
try {
buf.append("{name=").append(name);
buf.append(", dataType=");
dataType.output(buf, typesInProcess);
buf.append(", multiplicity=").append(multiplicity.toString());
buf.append(", isComposite=").append(Boolean.toString(isComposite));
buf.append(", isUnique=").append(Boolean.toString(isUnique));
buf.append(", isIndexable=").append(Boolean.toString(isIndexable));
buf.append(", reverseAttributeName=").append(reverseAttributeName);
buf.append('}');
}
catch(IOException e) {
throw new AtlasException(e);
}
}
@Override
......
......@@ -41,6 +41,7 @@ import java.security.MessageDigest;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ClassType extends HierarchicalType<ClassType, IReferenceableInstance>
implements IConstructableType<IReferenceableInstance, ITypedReferenceableInstance> {
......@@ -207,8 +208,8 @@ public class ClassType extends HierarchicalType<ClassType, IReferenceableInstanc
}
@Override
public void output(IReferenceableInstance s, Appendable buf, String prefix) throws AtlasException {
fieldMapping.output(s, buf, prefix);
public void output(IReferenceableInstance s, Appendable buf, String prefix, Set<IReferenceableInstance> inProcess) throws AtlasException {
fieldMapping.output(s, buf, prefix, inProcess);
}
@Override
......
......@@ -38,6 +38,7 @@ import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DataTypes {
......@@ -425,7 +426,7 @@ public class DataTypes {
}
@Override
public void output(Date val, Appendable buf, String prefix) throws AtlasException {
public void output(Date val, Appendable buf, String prefix, Set<Date> inProcess) throws AtlasException {
TypeUtils.outputVal(val == null ? "<null>" : TypeSystem.getInstance().getDateFormat().format(val), buf,
prefix);
}
......
......@@ -23,7 +23,9 @@ import org.apache.atlas.typesystem.IReferenceableInstance;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.persistence.Id;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class FieldMapping {
......@@ -70,7 +72,7 @@ public class FieldMapping {
this.numReferenceables = numReferenceables;
}
protected void outputFields(IStruct s, Appendable buf, String fieldPrefix) throws AtlasException {
protected void outputFields(IStruct s, Appendable buf, String fieldPrefix, Set<? extends IStruct> inProcess) throws AtlasException {
for (Map.Entry<String, AttributeInfo> e : fields.entrySet()) {
String attrName = e.getKey();
AttributeInfo i = e.getValue();
......@@ -79,32 +81,58 @@ public class FieldMapping {
if (aVal != null && aVal instanceof Id) {
TypeUtils.outputVal(aVal.toString(), buf, "");
} else {
i.dataType().output(aVal, buf, fieldPrefix);
i.dataType().output(aVal, buf, fieldPrefix, inProcess);
}
TypeUtils.outputVal("\n", buf, "");
}
}
public void output(IStruct s, Appendable buf, String prefix) throws AtlasException {
public void output(IStruct s, Appendable buf, String prefix, Set<IStruct> inProcess) throws AtlasException {
if (s == null) {
TypeUtils.outputVal("<null>\n", buf, "");
return;
}
if (inProcess == null) {
inProcess = new HashSet<>();
}
else if (inProcess.contains(s)) {
// Avoid infinite recursion when structs reference each other.
return;
}
inProcess.add(s);
try {
TypeUtils.outputVal("{", buf, prefix);
TypeUtils.outputVal("\n", buf, "");
String fieldPrefix = prefix + "\t";
outputFields(s, buf, fieldPrefix);
outputFields(s, buf, fieldPrefix, inProcess);
TypeUtils.outputVal("}", buf, prefix);
}
finally {
inProcess.remove(s);
}
}
public void output(IReferenceableInstance s, Appendable buf, String prefix) throws AtlasException {
public void output(IReferenceableInstance s, Appendable buf, String prefix, Set<IReferenceableInstance> inProcess) throws AtlasException {
if (s == null) {
TypeUtils.outputVal("<null>\n", buf, "");
return;
}
if (inProcess == null) {
inProcess = new HashSet<>();
}
else if (inProcess.contains(s)) {
// Avoid infinite recursion when structs reference each other.
return;
}
inProcess.add(s);
try {
TypeUtils.outputVal("{", buf, prefix);
TypeUtils.outputVal("\n", buf, "");
......@@ -114,17 +142,21 @@ public class FieldMapping {
TypeUtils.outputVal(s.getId().toString(), buf, "");
TypeUtils.outputVal("\n", buf, "");
outputFields(s, buf, fieldPrefix);
outputFields(s, buf, fieldPrefix, inProcess);
TypeSystem ts = TypeSystem.getInstance();
for (String sT : s.getTraits()) {
TraitType tt = ts.getDataType(TraitType.class, sT);
TypeUtils.outputVal(sT + " : ", buf, fieldPrefix);
tt.output(s.getTrait(sT), buf, fieldPrefix);
tt.output(s.getTrait(sT), buf, fieldPrefix, null);
}
TypeUtils.outputVal("}", buf, prefix);
}
finally {
inProcess.remove(s);
}
}
}
......@@ -21,12 +21,14 @@ package org.apache.atlas.typesystem.types;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.UnmodifiableIterator;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.persistence.DownCastStructInstance;
import org.apache.atlas.typesystem.types.TypeUtils.Pair;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
......@@ -367,9 +369,58 @@ public abstract class HierarchicalType<ST extends HierarchicalType, T> extends A
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
try {
output(buf, new HashSet<String>());
}
catch (AtlasException e) {
throw new RuntimeException(e);
}
return buf.toString();
}
@Override
public void output(Appendable buf, Set<String> typesInProcess) throws AtlasException {
if (typesInProcess == null) {
typesInProcess = new HashSet<>();
}
else if (typesInProcess.contains(name)) {
// Avoid infinite recursion on bi-directional reference attributes.
try {
buf.append(name);
} catch (IOException e) {
throw new AtlasException(e);
}
return;
}
return "[name=" + name + ", description=" + description +
", superTypes=" + superTypes + ", immediateAttrs=" + immediateAttrs + "]";
typesInProcess.add(name);
try {
buf.append(getClass().getSimpleName()).append('{');
buf.append("name=").append(name);
buf.append(", description=").append(description);
buf.append(", superTypes=").append(superTypes.toString());
buf.append(", immediateAttrs=[");
UnmodifiableIterator<AttributeInfo> it = immediateAttrs.iterator();
while (it.hasNext()) {
AttributeInfo attrInfo = it.next();
attrInfo.output(buf, typesInProcess);
if (it.hasNext()) {
buf.append(", ");
}
else {
buf.append(']');
}
}
buf.append("}");
}
catch(IOException e) {
throw new AtlasException(e);
}
finally {
typesInProcess.remove(name);
}
}
public Set<String> getAllSuperTypeNames() {
......
......@@ -21,6 +21,7 @@ package org.apache.atlas.typesystem.types;
import org.apache.atlas.AtlasException;
import java.security.MessageDigest;
import java.util.Set;
public interface IDataType<T> {
String getName();
......@@ -29,7 +30,25 @@ public interface IDataType<T> {
DataTypes.TypeCategory getTypeCategory();
void output(T val, Appendable buf, String prefix) throws AtlasException;
/**
* Output a string representation of a value instance of this type.
*
* @param val
* @param buf
* @param prefix
* @param inProcess
* @throws AtlasException
*/
void output(T val, Appendable buf, String prefix, Set<T> inProcess) throws AtlasException;
/**
* Output a string representation of this type.
*
* @param buf
* @param typesInProcess
* @throws AtlasException
*/
void output(Appendable buf, Set<String> typesInProcess) throws AtlasException;
void validateUpdate(IDataType newType) throws TypeUpdateException;
......
......@@ -79,8 +79,7 @@ public final class Multiplicity {
@Override
public String toString() {
return "Multiplicity{" +
"lower=" + lower +
return "{lower=" + lower +
", upper=" + upper +
", isUnique=" + isUnique +
'}';
......
......@@ -18,12 +18,16 @@
package org.apache.atlas.typesystem.types;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IStruct;
......@@ -182,8 +186,63 @@ public class StructType extends AbstractDataType<IStruct> implements IConstructa
}
@Override
public void output(IStruct s, Appendable buf, String prefix) throws AtlasException {
handler.output(s, buf, prefix);
public void output(IStruct s, Appendable buf, String prefix, Set<IStruct> inProcess) throws AtlasException {
handler.output(s, buf, prefix, inProcess);
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
try {
output(buf, new HashSet<String>());
}
catch (AtlasException e) {
throw new RuntimeException(e);
}
return buf.toString();
}
@Override
public void output(Appendable buf, Set<String> typesInProcess) throws AtlasException {
if (typesInProcess == null) {
typesInProcess = new HashSet<>();
}
else if (typesInProcess.contains(name)) {
// Avoid infinite recursion on bi-directional reference attributes.
try {
buf.append(name);
} catch (IOException e) {
throw new AtlasException(e);
}
return;
}
typesInProcess.add(name);
try {
buf.append(getClass().getSimpleName());
buf.append("{name=").append(name);
buf.append(", description=").append(description);
buf.append(", fieldMapping.fields=[");
Iterator<AttributeInfo> it = fieldMapping.fields.values().iterator();
while (it.hasNext()) {
AttributeInfo attrInfo = it.next();
attrInfo.output(buf, typesInProcess);
if (it.hasNext()) {
buf.append(", ");
}
else {
buf.append(']');
}
}
buf.append("}");
}
catch(IOException e) {
throw new AtlasException(e);
}
finally {
typesInProcess.remove(name);
}
}
@Override
......
......@@ -28,6 +28,7 @@ import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class TraitType extends HierarchicalType<TraitType, IStruct>
implements IConstructableType<IStruct, ITypedStruct> {
......@@ -63,8 +64,8 @@ public class TraitType extends HierarchicalType<TraitType, IStruct>
}
@Override
public void output(IStruct s, Appendable buf, String prefix) throws AtlasException {
handler.output(s, buf, prefix);
public void output(IStruct s, Appendable buf, String prefix, Set<IStruct> inProcess) throws AtlasException {
handler.output(s, buf, prefix, inProcess);
}
@Override
......
......@@ -20,6 +20,7 @@ package org.apache.atlas.typesystem.types;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.ITypedStruct;
......@@ -32,6 +33,7 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.Map;
import java.util.Set;
public class TypedStructHandler {
......@@ -104,8 +106,8 @@ public class TypedStructHandler {
fieldMapping.numReferenceables == 0 ? null : new Id[fieldMapping.numReferenceables]);
}
public void output(IStruct s, Appendable buf, String prefix) throws AtlasException {
fieldMapping.output(s, buf, prefix);
public void output(IStruct s, Appendable buf, String prefix, Set<IStruct> inProcess) throws AtlasException {
fieldMapping.output(s, buf, prefix, inProcess);
}
}
/**
* 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.atlas.typesystem.types;
import java.util.HashSet;
import org.apache.atlas.typesystem.IReferenceableInstance;
import org.apache.atlas.typesystem.ITypedReferenceableInstance;
import org.apache.atlas.typesystem.ITypedStruct;
import org.apache.atlas.typesystem.TypesDef;
import org.apache.atlas.typesystem.types.AttributeDefinition;
import org.apache.atlas.typesystem.types.ClassType;
import org.apache.atlas.typesystem.types.EnumTypeDefinition;
import org.apache.atlas.typesystem.types.HierarchicalTypeDefinition;
import org.apache.atlas.typesystem.types.Multiplicity;
import org.apache.atlas.typesystem.types.StructTypeDefinition;
import org.apache.atlas.typesystem.types.TraitType;
import org.apache.atlas.typesystem.types.TypeSystem;
import org.apache.atlas.typesystem.types.utils.TypesUtil;
import org.testng.Assert;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
/**
* Unit test for {@link FieldMapping}
*
*/
public class FieldMappingTest {
@BeforeTest
public void beforeTest() throws Exception {
TypeSystem typeSystem = TypeSystem.getInstance();
typeSystem.reset();
}
@Test
public void testOutputReferenceableInstance() throws Exception {
// ATLAS-645: verify that FieldMapping.output(IReferenceableInstance)
// does not infinitely recurse when ITypedReferenceableInstance's reference each other.
HierarchicalTypeDefinition<ClassType> valueDef = TypesUtil.createClassTypeDef("Value",
ImmutableSet.<String>of(),
new AttributeDefinition("owner", "Owner", Multiplicity.OPTIONAL, false, null));
// Define class type with reference, where the value is a class reference to Value.
HierarchicalTypeDefinition<ClassType> ownerDef = TypesUtil.createClassTypeDef("Owner",
ImmutableSet.<String>of(),
new AttributeDefinition("value", "Value", Multiplicity.OPTIONAL, false, null));
TypesDef typesDef = TypesUtil.getTypesDef(ImmutableList.<EnumTypeDefinition>of(),
ImmutableList.<StructTypeDefinition>of(), ImmutableList.<HierarchicalTypeDefinition<TraitType>>of(),
ImmutableList.of(ownerDef, valueDef));
TypeSystem typeSystem = TypeSystem.getInstance();
typeSystem.defineTypes(typesDef);
ClassType ownerType = typeSystem.getDataType(ClassType.class, "Owner");
// Prior to fix for ATLAS-645, this call would throw a StackOverflowError
try {
ownerType.toString();
}
catch (StackOverflowError e) {
Assert.fail("Infinite recursion in ClassType.toString() caused StackOverflowError");
}
ClassType valueType = typeSystem.getDataType(ClassType.class, "Value");
// Create instances of Owner and Value that reference each other.
ITypedReferenceableInstance ownerInstance = ownerType.createInstance();
ITypedReferenceableInstance valueInstance = valueType.createInstance();
// Set Owner.value reference to Value instance.
ownerInstance.set("value", valueInstance);
// Set Value.owner reference on Owner instance.
valueInstance.set("owner", ownerInstance);
// Prior to fix for ATLAS-645, this call would throw a StackOverflowError
try {
ownerInstance.fieldMapping().output(ownerInstance, new StringBuilder(), "", new HashSet<IReferenceableInstance>());
}
catch (StackOverflowError e) {
Assert.fail("Infinite recursion in FieldMapping.output() caused StackOverflowError");
}
}
@Test
public void testOutputStruct() throws Exception {
// ATLAS-645: verify that FieldMapping.output(IStruct) does not infinitely recurse
// when an IStruct and ITypedReferenceableInstance reference each other.
HierarchicalTypeDefinition<ClassType> valueDef = TypesUtil.createClassTypeDef("Value",
ImmutableSet.<String>of(),
new AttributeDefinition("owner", "Owner", Multiplicity.OPTIONAL, false, null));
// Define struct type with reference, where the value is a class reference to Value.
StructTypeDefinition ownerDef = TypesUtil.createStructTypeDef("Owner",
new AttributeDefinition("value", "Value", Multiplicity.OPTIONAL, false, null));
TypesDef typesDef = TypesUtil.getTypesDef(ImmutableList.<EnumTypeDefinition>of(),
ImmutableList.of(ownerDef), ImmutableList.<HierarchicalTypeDefinition<TraitType>>of(),
ImmutableList.of(valueDef));
TypeSystem typeSystem = TypeSystem.getInstance();
typeSystem.reset();
typeSystem.defineTypes(typesDef);
StructType ownerType = typeSystem.getDataType(StructType.class, "Owner");
ClassType valueType = typeSystem.getDataType(ClassType.class, "Value");
// Prior to fix for ATLAS-645, this call would throw a StackOverflowError
try {
ownerType.toString();
}
catch (StackOverflowError e) {
Assert.fail("Infinite recursion in StructType.toString() caused StackOverflowError");
}
// Create instances of Owner and Value that reference each other.
ITypedStruct ownerInstance = ownerType.createInstance();
ITypedReferenceableInstance valueInstance = valueType.createInstance();
// Set Owner.value reference to Value instance.
ownerInstance.set("value", valueInstance);
// Set Value.owner reference on Owner instance.
valueInstance.set("owner", ownerInstance);
// Prior to fix for ATLAS-645, this call would throw a StackOverflowError
try {
ownerInstance.fieldMapping().output(ownerInstance, new StringBuilder(), "", null);
}
catch (StackOverflowError e) {
Assert.fail("Infinite recursion in FieldMapping.output() caused StackOverflowError");
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment