Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support transformation schema for XML #549

Merged
merged 13 commits into from
Nov 14, 2022
Next Next commit
Add XML transformer
  • Loading branch information
RVRhub committed Nov 9, 2022
commit 5c8367afd3a793ac14644ea5fce8f71f531c0a43
1 change: 1 addition & 0 deletions src/main/java/net/datafaker/formats/Xml.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.HashMap;
import java.util.Map;

@Deprecated
public class Xml {
private static final Map<Character, String> ESCAPING_MAP = createEscapeMap();
private static final int INDENTATION_STEP = 4;
Expand Down
164 changes: 164 additions & 0 deletions src/main/java/net/datafaker/transformations/XmlTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package net.datafaker.transformations;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import net.datafaker.sequence.FakeSequence;

public class XmlTransformer<IN> implements Transformer<IN, CharSequence> {

private static final Map<Character, String> ESCAPING_MAP = createEscapeMap();
private static final int INDENTATION_STEP = 4;

private int tagIndex = 0;

private final boolean pretty;

private XmlTransformer(boolean pretty) {
this.pretty = pretty;
}

@Override
public CharSequence apply(IN input, Schema<IN, ?> schema) {
StringBuilder sb = new StringBuilder();
Arrays.stream(schema.getFields()).forEach(it -> apply(input, sb, it));
return sb.toString();
}

private void apply(IN input, StringBuilder sb, Field<IN, ?> xmlNode) {

if (pretty && tagIndex > 0) {
sb.append(System.lineSeparator()).append(offset(tagIndex));
}

final String tag = xmlNode.getName().trim();
sb.append("<").append(tag);
if (xmlNode instanceof CompositeField) {
Field<IN, ?>[] attrs = ((CompositeField) xmlNode).getFields();
applyAttributes(input, sb, attrs);

Field<IN, ?> content = Arrays.stream(attrs)
.filter(inField -> !isAttribute(inField.getName())).findFirst()
.orElse(null);
applyTag(input, sb, content, tag);
} else {
applyTag(input, sb, xmlNode, tag);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could a bit simpler like

Suggested change
Field<IN, ?> content = Arrays.stream(attrs)
.filter(inField -> !isAttribute(inField.getName())).findFirst()
.orElse(null);
applyTag(input, sb, content, tag);
} else {
applyTag(input, sb, xmlNode, tag);
}
xmlNode = Arrays.stream(attrs)
.filter(inField -> !isAttribute(inField.getName())).findFirst()
.orElse(null);
}
applyTag(input, sb, xmlNode, tag);

}

private void applyTag(IN input, StringBuilder sb, Field<IN, ?> field, String tag) {
if (field == null ) {
applyValue(sb, tag, null);
return;
}

Object xmlNodeValue = field.transform(input);
if (xmlNodeValue instanceof Collection) {
Collection<?> children = (Collection<?>) xmlNodeValue;
if (children.isEmpty()) {
applyValue(sb, tag, null);
} else {
sb.append(">");
tagIndex++;
for (Object xmlNodeElem : children) {
apply(input, sb, (Field<IN, ?>) xmlNodeElem);
}
tagIndex--;
if (pretty) {
sb.append(System.lineSeparator()).append(offset(tagIndex));
}
sb.append("</").append(tag).append(">");
}

} else if (xmlNodeValue instanceof String) {
applyValue(sb, tag, (String) xmlNodeValue);
} else if (xmlNodeValue == null) {
applyValue(sb, tag, null);
}
}

private boolean isAttribute(String name) {
return name != null;
}

private void applyAttributes(IN input, StringBuilder sb, Field<IN, ?>[] attrs) {
for (Field<IN, ?> attr : attrs) {
String name = attr.getName();
if(isAttribute(name)) {
String value = (String) attr.transform(input);
sb.append(" ").append(name).append("=\"").append(escape(value)).append("\"");
}
}
}

private void applyValue(StringBuilder sb, String tag, String xmlNodeValue) {
if (xmlNodeValue != null) {
sb.append(">");
sb.append(escape(xmlNodeValue));
sb.append("</").append(tag).append(">");
} else {
sb.append("/>");
}
}

@Override
public CharSequence generate(FakeSequence<IN> input, Schema<IN, ?> schema) {
return null;
}

@Override
public CharSequence generate(Schema<IN, ?> schema, int limit) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < limit; i++) {
sb.append(apply(null, schema));
if (i < limit - 1) {
sb.append(LINE_SEPARATOR);
}
}
return sb.toString();
}

public static class XmlTransformerBuilder<IN> {

private boolean pretty= false;

public XmlTransformer.XmlTransformerBuilder<IN> pretty(boolean pretty) {
this.pretty = pretty;
return this;
}

public XmlTransformer<IN> build() {
return new XmlTransformer<>(pretty);
}
}

private String offset(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length * INDENTATION_STEP; i++) {
sb.append(" ");
}
return sb.toString();
}

private String escape(String str) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
final char c = str.charAt(i);
sb.append(ESCAPING_MAP.getOrDefault(c, c + ""));
}
return sb.toString();
}

private static Map<Character, String> createEscapeMap() {
final Map<Character, String> map = new HashMap<>();
map.put('<', "&lt;");
map.put('>', "&gt;");
map.put('&', "&amp;");
map.put('\'', "&apos;");
map.put('"', "&quot;");
return Collections.unmodifiableMap(map);
}
}
63 changes: 63 additions & 0 deletions src/test/java/net/datafaker/formats/XmlTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package net.datafaker.formats;

import net.datafaker.transformations.Field;
import net.datafaker.transformations.Schema;
import net.datafaker.transformations.XmlTransformer;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
Expand All @@ -10,9 +13,69 @@
import java.util.Map;
import java.util.stream.Stream;

import static net.datafaker.transformations.Field.compositeField;
import static net.datafaker.transformations.Field.field;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.of;

class XmlTest {

@ParameterizedTest
@MethodSource("generateTestXmlSchema")
void xmlSchemaTest(Schema<String, String> schema, String expected) {
XmlTransformer<String> xml = new XmlTransformer.XmlTransformerBuilder<String>().build();
assertThat(xml.generate(schema, 1)).isEqualTo(expected);
}

private static Stream<Arguments> generateTestXmlSchema() {
return Stream.of(
of(Schema.of(field("root", Collections::emptyList)), "<root/>"),
of(Schema.of((field("root", () -> "value"))), "<root>value</root>"),
of(Schema.of(
compositeField("root",
new Field[]{field("attribute1", () -> "value1"), field("attribute2", () -> "value2")})),
"<root attribute1=\"value1\" attribute2=\"value2\"/>"),
of(Schema.of(
compositeField("root",
new Field[]{field("attribute1", () -> "value1"), field("attribute2", () -> "value2"), field(null, () -> "value"),})),
"<root attribute1=\"value1\" attribute2=\"value2\">value</root>"),
of(Schema.of(
compositeField("root",
new Field[]{field("attribute1", () -> "value1"), field("attribute2", () -> "value2"),
field(null, () -> Collections.singletonList(field("child", () -> "value")))})),
"<root attribute1=\"value1\" attribute2=\"value2\"><child>value</child></root>"),
of(Schema.of(field("root", () -> "<> value\"")), "<root>&lt;&gt; value&quot;</root>")
);
}

@ParameterizedTest
@MethodSource("generateTestXmlPrettySchema")
void xmlPrettySchemaTest(Schema<String, String> schema, String expected) {
XmlTransformer<String> xml = new XmlTransformer.XmlTransformerBuilder<String>().pretty(true).build();
assertThat(xml.generate(schema, 1)).isEqualTo(expected);
}

private static Stream<Arguments> generateTestXmlPrettySchema() {
return Stream.of(
of(Schema.of(field("root", Collections::emptyList)), "<root/>"),
of(Schema.of((field("root", () -> "value"))), "<root>value</root>"),
of(Schema.of(
compositeField("root",
new Field[]{field("attribute1", () -> "value1"), field("attribute2", () -> "value2")})),
"<root attribute1=\"value1\" attribute2=\"value2\"/>"),
of(Schema.of(
compositeField("root",
new Field[]{field("attribute1", () -> "value1"), field("attribute2", () -> "value2"), field(null, () -> "value"),})),
"<root attribute1=\"value1\" attribute2=\"value2\">value</root>"),
of(Schema.of(
compositeField("root",
new Field[]{field("attribute1", () -> "value1"), field("attribute2", () -> "value2"),
field(null, () -> Collections.singletonList(field("child", () -> "value")))})),
"<root attribute1=\"value1\" attribute2=\"value2\">" + System.lineSeparator() + " <child>value</child>" + System.lineSeparator() + "</root>"),
of(Schema.of(field("root", () -> "<> value\"")), "<root>&lt;&gt; value&quot;</root>")
);
}

@ParameterizedTest
@MethodSource("generateTestXmlPretty")
void xmlPrettyTest(Xml.XmlNode xmlNode, String expected) {
Expand Down