| /* |
| * ==================================================================== |
| * 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("<"); |
| break; |
| case ">": |
| fw.write(">"); |
| break; |
| case "&": |
| fw.write("&"); |
| break; |
| case "\'": |
| fw.write("'"); |
| break; |
| case "\"": |
| fw.write("""); |
| 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); |
| } |
| |
| } |