blob: c5a949cc6383eef89319a2174ac2eb7909300e1c [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.poi.util;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.util.GenericRecordJsonWriter.AppendableWriter;
import org.apache.poi.util.GenericRecordJsonWriter.NullOutputStream;
@SuppressWarnings("WeakerAccess")
public class GenericRecordXmlWriter implements Closeable {
private static final String TABS;
private static final String ZEROS = "0000000000000000";
private static final Pattern ESC_CHARS = Pattern.compile("[<>&'\"\\p{Cntrl}]");
@FunctionalInterface
protected interface GenericRecordHandler {
/**
* Handler method
*
* @param record the parent record, applied via instance method reference
* @param name the name of the property
* @param object the value of the property
* @return {@code true}, if the element was handled and output produced,
* The provided methods can be overridden and a implementation can return {@code false},
* if the element hasn't been written to the stream
*/
boolean print(GenericRecordXmlWriter record, String name, Object object);
}
private static final List<Map.Entry<Class, GenericRecordHandler>> handler = new ArrayList<>();
static {
char[] t = new char[255];
Arrays.fill(t, '\t');
TABS = new String(t);
handler(String.class, GenericRecordXmlWriter::printObject);
handler(Number.class, GenericRecordXmlWriter::printNumber);
handler(Boolean.class, GenericRecordXmlWriter::printBoolean);
handler(List.class, GenericRecordXmlWriter::printList);
// handler(GenericRecord.class, GenericRecordXmlWriter::printGenericRecord);
handler(GenericRecordUtil.AnnotatedFlag.class, GenericRecordXmlWriter::printAnnotatedFlag);
handler(byte[].class, GenericRecordXmlWriter::printBytes);
handler(Point2D.class, GenericRecordXmlWriter::printPoint);
handler(Dimension2D.class, GenericRecordXmlWriter::printDimension);
handler(Rectangle2D.class, GenericRecordXmlWriter::printRectangle);
handler(Path2D.class, GenericRecordXmlWriter::printPath);
handler(AffineTransform.class, GenericRecordXmlWriter::printAffineTransform);
handler(Color.class, GenericRecordXmlWriter::printColor);
handler(BufferedImage.class, GenericRecordXmlWriter::printBufferedImage);
handler(Array.class, GenericRecordXmlWriter::printArray);
handler(Object.class, GenericRecordXmlWriter::printObject);
}
private static void handler(Class c, GenericRecordHandler printer) {
handler.add(new AbstractMap.SimpleEntry<>(c, printer));
}
private final PrintWriter fw;
private int indent = 0;
private boolean withComments = true;
private int childIndex = 0;
private boolean attributePhase = true;
public GenericRecordXmlWriter(File fileName) throws IOException {
OutputStream os = ("null".equals(fileName.getName())) ? new NullOutputStream() : new FileOutputStream(fileName);
fw = new PrintWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
}
public GenericRecordXmlWriter(Appendable buffer) {
fw = new PrintWriter(new AppendableWriter(buffer));
}
public static String marshal(GenericRecord record) {
return marshal(record, true);
}
public static String marshal(GenericRecord record, boolean withComments) {
final StringBuilder sb = new StringBuilder();
try (GenericRecordXmlWriter w = new GenericRecordXmlWriter(sb)) {
w.setWithComments(withComments);
w.write(record);
return sb.toString();
} catch (IOException e) {
return "<record/>";
}
}
public void setWithComments(boolean withComments) {
this.withComments = withComments;
}
@Override
public void close() throws IOException {
fw.close();
}
protected String tabs() {
return TABS.substring(0, Math.min(indent, TABS.length()));
}
public void write(GenericRecord record) {
write("record", record);
}
protected void write(final String name, GenericRecord record) {
final String tabs = tabs();
Enum type = record.getGenericRecordType();
String recordName = (type != null) ? type.name() : record.getClass().getSimpleName();
fw.append(tabs);
fw.append("<"+name+" type=\"");
fw.append(recordName);
fw.append("\"");
if (childIndex > 0) {
fw.append(" index=\"");
fw.print(childIndex);
fw.append("\"");
}
attributePhase = true;
boolean hasComplex = writeProperties(record);
attributePhase = false;
hasComplex |= writeChildren(record, hasComplex);
if (hasComplex) {
fw.append(tabs);
fw.println("</" + name + ">");
} else {
fw.println("/>");
}
}
protected boolean writeProperties(GenericRecord record) {
Map<String, Supplier<?>> prop = record.getGenericProperties();
if (prop == null || prop.isEmpty()) {
return false;
}
final int oldChildIndex = childIndex;
childIndex = 0;
List<Map.Entry<String,Supplier<?>>> complex = prop.entrySet().stream().flatMap(this::writeProp).collect(Collectors.toList());
attributePhase = false;
if (!complex.isEmpty()) {
fw.println(">");
indent++;
complex.forEach(this::writeProp);
indent--;
}
childIndex = oldChildIndex;
return !complex.isEmpty();
}
protected boolean writeChildren(GenericRecord record, boolean hasComplexProperties) {
List<? extends GenericRecord> list = record.getGenericChildren();
if (list == null || list.isEmpty()) {
return false;
}
if (!hasComplexProperties) {
fw.print(">");
}
indent++;
fw.println();
fw.println(tabs()+"<children>");
indent++;
final int oldChildIndex = childIndex;
childIndex = 0;
list.forEach(l -> {
writeValue("record", l);
childIndex++;
});
childIndex = oldChildIndex;
fw.println();
indent--;
fw.println(tabs()+"</children>");
indent--;
return true;
}
public void writeError(String errorMsg) {
printObject("error", errorMsg);
}
protected Stream<Map.Entry<String,Supplier<?>>> writeProp(Map.Entry<String,Supplier<?>> me) {
Object obj = me.getValue().get();
if (obj == null) {
return Stream.empty();
}
final boolean isComplex = isComplex(obj);
if (attributePhase == isComplex) {
return isComplex ? Stream.of(new AbstractMap.SimpleEntry<>(me.getKey(), () -> obj)) : Stream.empty();
}
final int oldChildIndex = childIndex;
childIndex = 0;
writeValue(me.getKey(), obj);
childIndex = oldChildIndex;
return Stream.empty();
}
protected static boolean isComplex(Object obj) {
return !(
obj instanceof Number ||
obj instanceof Boolean ||
obj instanceof Character ||
obj instanceof String ||
obj instanceof Color ||
obj instanceof Enum);
}
protected void writeValue(String name, Object value) {
assert(name != null);
if (value instanceof GenericRecord) {
printGenericRecord(name, value);
} else if (value != null) {
if (name.endsWith(">")) {
fw.print("\t");
}
handler.stream().
filter(h -> matchInstanceOrArray(h.getKey(), value)).
findFirst().
ifPresent(h -> h.getValue().print(this, name, value));
}
}
protected static boolean matchInstanceOrArray(Class key, Object instance) {
return key.isInstance(instance) || (Array.class.equals(key) && instance.getClass().isArray());
}
protected void openName(String name) {
name = name.replace(">>", ">");
if (attributePhase) {
fw.print(" " + name.replace('>',' ').trim() + "=\"");
} else {
fw.print(tabs() + "<" + name);
if (name.endsWith(">")) {
fw.println();
}
}
}
protected void closeName(String name) {
name = name.replace(">>", ">");
if (attributePhase) {
fw.append("\"");
} else {
if (name.endsWith(">")) {
fw.println(tabs() + "\t</" + name);
} else {
fw.println("/>");
}
}
}
protected boolean printNumber(String name, Object o) {
assert(attributePhase);
openName(name);
Number n = (Number)o;
fw.print(n.toString());
closeName(name);
return true;
}
protected boolean printBoolean(String name, Object o) {
assert (attributePhase);
openName(name);
fw.write(((Boolean)o).toString());
closeName(name);
return true;
}
protected boolean printList(String name, Object o) {
assert (!attributePhase);
openName(name+">");
int oldChildIndex = childIndex;
childIndex = 0;
//noinspection unchecked
((List)o).forEach(e -> { writeValue("item>", e); childIndex++; });
childIndex = oldChildIndex;
closeName(name+">");
return true;
}
protected boolean printArray(String name, Object o) {
assert (!attributePhase);
openName(name+">");
int length = Array.getLength(o);
final int oldChildIndex = childIndex;
for (childIndex=0; childIndex<length; childIndex++) {
writeValue("item>", Array.get(o, childIndex));
}
childIndex = oldChildIndex;
closeName(name+">");
return true;
}
protected void printGenericRecord(String name, Object value) {
write(name, (GenericRecord) value);
}
protected boolean printAnnotatedFlag(String name, Object o) {
assert (!attributePhase);
GenericRecordUtil.AnnotatedFlag af = (GenericRecordUtil.AnnotatedFlag) o;
Number n = af.getValue().get();
int len;
if (n instanceof Byte) {
len = 2;
} else if (n instanceof Short) {
len = 4;
} else if (n instanceof Integer) {
len = 8;
} else {
len = 16;
}
openName(name);
fw.print(" flag=\"0x");
fw.print(trimHex(n.longValue(), len));
fw.print('"');
if (withComments) {
fw.print(" description=\"");
fw.print(af.getDescription());
fw.print("\"");
}
closeName(name);
return true;
}
protected boolean printBytes(String name, Object o) {
assert (!attributePhase);
openName(name+">");
fw.write(Base64.getEncoder().encodeToString((byte[]) o));
closeName(name+">");
return true;
}
protected boolean printPoint(String name, Object o) {
assert (!attributePhase);
openName(name);
Point2D p = (Point2D)o;
fw.println(" x=\""+p.getX()+"\" y=\""+p.getY()+"\"/>");
closeName(name);
return true;
}
protected boolean printDimension(String name, Object o) {
assert (!attributePhase);
openName(name);
Dimension2D p = (Dimension2D)o;
fw.println(" width=\""+p.getWidth()+"\" height=\""+p.getHeight()+"\"/>");
closeName(name);
return true;
}
protected boolean printRectangle(String name, Object o) {
assert (!attributePhase);
openName(name);
Rectangle2D p = (Rectangle2D)o;
fw.println(" x=\""+p.getX()+"\" y=\""+p.getY()+"\" width=\""+p.getWidth()+"\" height=\""+p.getHeight()+"\"/>");
closeName(name);
return true;
}
protected boolean printPath(String name, Object o) {
assert (!attributePhase);
openName(name+">");
final PathIterator iter = ((Path2D)o).getPathIterator(null);
final double[] pnts = new double[6];
indent += 2;
String t = tabs();
indent -= 2;
while (!iter.isDone()) {
fw.print(t);
final int segType = iter.currentSegment(pnts);
fw.print("<pathelement ");
switch (segType) {
case PathIterator.SEG_MOVETO:
fw.print("type=\"move\" x=\""+pnts[0]+"\" y=\""+pnts[1]+"\"");
break;
case PathIterator.SEG_LINETO:
fw.print("type=\"lineto\" x=\""+pnts[0]+"\" y=\""+pnts[1]+"\"");
break;
case PathIterator.SEG_QUADTO:
fw.print("type=\"quad\" x1=\""+pnts[0]+"\" y1=\""+pnts[1]+"\" x2=\""+pnts[2]+"\" y2=\""+pnts[3]+"\"");
break;
case PathIterator.SEG_CUBICTO:
fw.print("type=\"cubic\" x1=\""+pnts[0]+"\" y1=\""+pnts[1]+"\" x2=\""+pnts[2]+"\" y2=\""+pnts[3]+"\" x3=\""+pnts[4]+"\" y3=\""+pnts[5]+"\"");
break;
case PathIterator.SEG_CLOSE:
fw.print("type=\"close\"");
break;
}
fw.println("/>");
iter.next();
}
closeName(name+">");
return true;
}
protected boolean printObject(String name, Object o) {
openName(name+">");
final String str = o.toString();
final Matcher m = ESC_CHARS.matcher(str);
int pos = 0;
while (m.find()) {
fw.write(str, pos, m.start());
String match = m.group();
switch (match) {
case "<":
fw.write("&lt;");
break;
case ">":
fw.write("&gt;");
break;
case "&":
fw.write("&amp;");
break;
case "\'":
fw.write("&apos;");
break;
case "\"":
fw.write("&quot;");
break;
default:
fw.write("&#x");
fw.write(Long.toHexString(match.codePointAt(0)));
fw.write(";");
break;
}
pos = m.end();
}
fw.append(str, pos, str.length());
closeName(name+">");
return true;
}
protected boolean printAffineTransform(String name, Object o) {
assert (!attributePhase);
openName(name);
AffineTransform xForm = (AffineTransform)o;
fw.write("<"+name+
" scaleX=\""+xForm.getScaleX()+"\" "+
"shearX=\""+xForm.getShearX()+"\" "+
"transX=\""+xForm.getTranslateX()+"\" "+
"scaleY=\""+xForm.getScaleY()+"\" "+
"shearY=\""+xForm.getShearY()+"\" "+
"transY=\""+xForm.getTranslateY()+"\"/>");
closeName(name);
return true;
}
protected boolean printColor(String name, Object o) {
assert (attributePhase);
openName(name);
final int rgb = ((Color)o).getRGB();
fw.print("0x"+trimHex(rgb, 8));
closeName(name);
return true;
}
protected boolean printBufferedImage(String name, Object o) {
assert (!attributePhase);
openName(name);
BufferedImage bi = (BufferedImage)o;
fw.println(" width=\""+bi.getWidth()+"\" height=\""+bi.getHeight()+"\" bands=\""+bi.getColorModel().getNumComponents()+"\"");
closeName(name);
return true;
}
protected String trimHex(final long l, final int size) {
final String b = Long.toHexString(l);
int len = b.length();
return ZEROS.substring(0, Math.max(0,size-len)) + b.substring(Math.max(0,len-size), len);
}
}