// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.prelude.hitfield; import com.yahoo.text.XML; import com.yahoo.data.access.Inspector; import com.yahoo.data.access.Type; import java.util.Map; /** * Utility class for converting accessible data into the historical "prelude" xml format. */ public class XmlRenderer { public static StringBuilder render(StringBuilder target, Inspector value) { new InspectorRenderer(target).renderInspector(value, 2); return target; } private static class InspectorRenderer { private final StringBuilder renderTarget; InspectorRenderer(StringBuilder target) { this.renderTarget = target; } void renderInspector(Inspector value, int nestingLevel) { if (value.type() == Type.ARRAY) { renderMapOrArray(value, nestingLevel); } else if (value.type() == Type.OBJECT) { renderStruct(value, nestingLevel); } else if (value.type() == Type.STRING) { renderTarget.append(XML.xmlEscape(value.asString(), false)); } else if (value.type() == Type.LONG) { long l = value.asLong(); renderTarget.append(String.valueOf(l)); } else if (value.type() == Type.DOUBLE) { double d = value.asDouble(); renderTarget.append(String.valueOf(d)); } else if (value.type() == Type.BOOL) { boolean b = value.asBool(); renderTarget.append(b ? "true" : "false"); } else if (value.type() == Type.DATA) { byte[] data = value.asData(); renderTarget.append(""); for (byte datum : data) { for (int sh = 4; sh >= 0; sh -= 4) { int val = (datum >> sh) & 0xF; char hexdigit = (val < 10) ? ((char) ('0' + val)) : ((char) ('A' + val - 10)); renderTarget.append(hexdigit); } } renderTarget.append(""); } } private void renderMapItem(Inspector object, int nestingLevel) { renderTarget.append('\n'); indent(nestingLevel); renderTarget.append(""); renderInspector(object.field("key"), nestingLevel); renderTarget.append(""); renderInspector(object.field("value"), nestingLevel); renderTarget.append(""); } private void renderStructure(Inspector structure, int nestingLevel) { for (Map.Entry entry : structure.fields()) { String key = entry.getKey(); Inspector value = entry.getValue(); renderTarget.append('\n'); indent(nestingLevel); renderTarget.append(""); renderInspector(value, nestingLevel); renderTarget.append(""); } renderTarget.append('\n'); } private void renderStruct(Inspector object, int nestingLevel) { renderStructure(object, nestingLevel + 1); indent(nestingLevel); } private void indent(int nestingLevel) { renderTarget.append(" ".repeat(Math.max(0, nestingLevel))); } private void renderMap(Inspector sequence, int nestingLevel) { int limit = sequence.entryCount(); if (limit == 0) return; for (int i = 0; i < limit; ++i) renderMapItem(sequence.entry(i), nestingLevel); renderTarget.append("\n"); } /** Returns true if the given array represents a map - a list of pairs called "key" and "value" */ private boolean isMap(Inspector array) { Inspector firstObject = array.entry(0); if (firstObject.type() != Type.OBJECT) return false; if (firstObject.fieldCount() != 2) return false; if (! firstObject.field("key").valid()) return false; if (! firstObject.field("value").valid()) return false; return true; } /** * Returns true if the given array represents a weighted set, * as a list of pairs called "item" and "weight" **/ private boolean isWeightedSetObjects(Inspector array) { Inspector firstObject = array.entry(0); if (firstObject.type() != Type.OBJECT) return false; if (firstObject.fieldCount() != 2) return false; if (! firstObject.field("item").valid()) return false; if (! firstObject.field("weight").valid()) return false; return true; } /** * Returns true if the given array represents a weighted set, * as a list of tuples **/ private boolean isWeightedSetArrays(Inspector array) { Inspector firstObject = array.entry(0); if (firstObject.type() != Type.ARRAY) return false; if (firstObject.entryCount() != 2) return false; return true; } private void renderMapOrArray(Inspector sequence, int nestingLevel) { if (sequence.entryCount() == 0) return; if (isMap(sequence)) { renderMap(sequence, nestingLevel + 1); } else if (isWeightedSetArrays(sequence)) { renderWeightedSet(sequence, nestingLevel + 1, true); } else if (isWeightedSetObjects(sequence)) { renderWeightedSet(sequence, nestingLevel + 1, false); } else { renderArray(sequence, nestingLevel + 1); } indent(nestingLevel); } private void renderWeightedSet(Inspector seq, int nestingLevel, boolean nestedarray) { int limit = seq.entryCount(); renderTarget.append('\n'); for (int i = 0; i < limit; ++i) { Inspector value = nestedarray ? seq.entry(i).entry(0) : seq.entry(i).field("item"); Inspector weight = nestedarray ? seq.entry(i).entry(1) : seq.entry(i).field("weight"); long lw = 0; double dw = 0; if (weight.type() == Type.LONG) { lw = weight.asLong(); dw = (double)lw; } if (weight.type() == Type.DOUBLE) { dw = weight.asDouble(); lw = (long)dw; } indent(nestingLevel); renderTarget.append(""); renderInspector(value, nestingLevel); renderTarget.append("\n"); } } private void renderArray(Inspector seq, int nestingLevel) { int limit = seq.entryCount(); if (limit == 0) return; renderTarget.append('\n'); for (int i = 0; i < limit; ++i) { Inspector value = seq.entry(i); indent(nestingLevel); renderTarget.append(""); renderInspector(value, nestingLevel); renderTarget.append("\n"); } } } }