| /******************************************************************************* |
| * Copyright (c) 2007, 2017 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.equinox.internal.p2.persistence; |
| |
| import static java.util.stream.Collectors.joining; |
| |
| import java.io.*; |
| import java.nio.charset.StandardCharsets; |
| import java.util.*; |
| import org.eclipse.equinox.p2.metadata.Version; |
| |
| public class XMLWriter implements XMLConstants { |
| |
| static final boolean ignoreWhitespace = Boolean.getBoolean("p2.ignoreWhitespace"); //$NON-NLS-1$ |
| |
| public static class ProcessingInstruction { |
| |
| private String target; |
| private String[] data; |
| |
| // The standard UTF-8 processing instruction |
| public static final String XML_UTF8 = "<?xml version='1.0' encoding='UTF-8'?>"; //$NON-NLS-1$ |
| |
| public ProcessingInstruction(String target, String[] attrs, String[] values) { |
| // Lengths of attributes and values must be the same |
| this.target = target; |
| this.data = new String[attrs.length]; |
| for (int i = 0; i < attrs.length; i++) { |
| data[i] = attributeImage(attrs[i], values[i]); |
| } |
| } |
| |
| public static ProcessingInstruction makeTargetVersionInstruction(String target, Version version) { |
| return new ProcessingInstruction(target, new String[] {PI_VERSION_ATTRIBUTE}, new String[] {version.toString()}); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder("<?"); //$NON-NLS-1$ |
| sb.append(this.target).append(' '); |
| for (int i = 0; i < data.length; i++) { |
| sb.append(this.data[i]); |
| if (i < data.length - 1) { |
| sb.append(' '); |
| } |
| } |
| sb.append("?>"); //$NON-NLS-1$ |
| return sb.toString(); |
| } |
| } |
| |
| private Stack<String> elements; // XML elements that have not yet been closed |
| private boolean open; // Can attributes be added to the current element? |
| private String indent; // used for each level of indentation |
| |
| private PrintWriter pw; |
| |
| public XMLWriter(OutputStream output, ProcessingInstruction[] piElements) { |
| this.pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)), false); |
| println(ProcessingInstruction.XML_UTF8); |
| this.elements = new Stack<>(); |
| this.open = false; |
| this.indent = " "; //$NON-NLS-1$ |
| if (piElements != null) { |
| for (ProcessingInstruction piElement : piElements) { |
| println(piElement.toString()); |
| } |
| } |
| } |
| |
| // start a new element |
| public void start(String name) { |
| if (this.open) { |
| println('>'); |
| } |
| if (!ignoreWhitespace) { |
| indent(); |
| } |
| print('<'); |
| print(name); |
| this.elements.push(name); |
| this.open = true; |
| } |
| |
| // end the most recent element with this name |
| public void end(String name) { |
| if (this.elements.empty()) { |
| throw new EndWithoutStartError(); |
| } |
| int index = this.elements.search(name); |
| if (index == -1) { |
| throw new EndWithoutStartError(name); |
| } |
| for (int i = 0; i < index; i += 1) { |
| end(); |
| } |
| } |
| |
| // end the current element |
| public void end() { |
| if (this.elements.empty()) { |
| throw new EndWithoutStartError(); |
| } |
| String name = this.elements.pop(); |
| if (this.open) { |
| println("/>"); //$NON-NLS-1$ |
| } else { |
| printlnIndented("</" + name + '>', false); //$NON-NLS-1$ |
| } |
| this.open = false; |
| } |
| |
| public static String escape(String txt) { |
| StringBuffer buffer = null; |
| for (int i = 0; i < txt.length(); ++i) { |
| String replace; |
| char c = txt.charAt(i); |
| switch (c) { |
| case '<' : |
| replace = "<"; //$NON-NLS-1$ |
| break; |
| case '>' : |
| replace = ">"; //$NON-NLS-1$ |
| break; |
| case '"' : |
| replace = """; //$NON-NLS-1$ |
| break; |
| case '\'' : |
| replace = "'"; //$NON-NLS-1$ |
| break; |
| case '&' : |
| replace = "&"; //$NON-NLS-1$ |
| break; |
| case '\t' : |
| replace = "	"; //$NON-NLS-1$ |
| break; |
| case '\n' : |
| replace = "
"; //$NON-NLS-1$ |
| break; |
| case '\r' : |
| replace = "
"; //$NON-NLS-1$ |
| break; |
| default : |
| // this is the set of legal xml scharacters in unicode excluding high surrogates since they cannot be represented with a char |
| // see http://www.w3.org/TR/REC-xml/#charsets |
| if ((c >= '\u0020' && c <= '\uD7FF') || (c >= '\uE000' && c <= '\uFFFD')) { |
| if (buffer != null) |
| buffer.append(c); |
| continue; |
| } |
| replace = Character.isWhitespace(c) ? " " : null; //$NON-NLS-1$ |
| } |
| if (buffer == null) { |
| buffer = new StringBuffer(txt.length() + 16); |
| buffer.append(txt.substring(0, i)); |
| } |
| if (replace != null) |
| buffer.append(replace); |
| } |
| |
| if (buffer == null) |
| return txt; |
| |
| return buffer.toString(); |
| } |
| |
| // write a boolean attribute if it doesn't have the default value |
| public void attribute(String name, boolean value, boolean defaultValue) { |
| if (value != defaultValue) { |
| attribute(name, value); |
| } |
| } |
| |
| public void attribute(String name, boolean value) { |
| attribute(name, Boolean.toString(value)); |
| } |
| |
| public void attribute(String name, int value) { |
| attribute(name, Integer.toString(value)); |
| } |
| |
| public void attributeOptional(String name, String value) { |
| if (value != null && value.length() > 0) { |
| attribute(name, value); |
| } |
| } |
| |
| public void attribute(String name, Object value) { |
| if (!this.open) { |
| throw new AttributeAfterNestedContentError(); |
| } |
| if (value == null) { |
| return; // optional attribute with no value |
| } |
| print(' '); |
| print(name); |
| print("='"); //$NON-NLS-1$ |
| print(escape(value.toString())); |
| print('\''); |
| } |
| |
| public void cdata(String data) { |
| cdata(data, true); |
| } |
| |
| public void cdata(String data, boolean escape) { |
| if (this.open) { |
| println('>'); |
| this.open = false; |
| } |
| if (data != null) { |
| printlnIndented(data, escape); |
| } |
| } |
| |
| public void flush() { |
| this.pw.flush(); |
| } |
| |
| public void writeProperties(Map<String, ?> properties) { |
| writeProperties(PROPERTIES_ELEMENT, properties); |
| } |
| |
| public void writeProperties(String propertiesElement, Map<String, ?> properties) { |
| if (properties == null || properties.isEmpty()) { |
| return; |
| } |
| |
| start(propertiesElement); |
| attribute(COLLECTION_SIZE_ATTRIBUTE, properties.size()); |
| properties.forEach(this::writeProperty); |
| end(); |
| } |
| |
| public void writeProperty(String name, Object value) { |
| String type; |
| String valueStr; |
| |
| if (Collection.class.isAssignableFrom(value.getClass())) { |
| Collection<?> coll = (Collection<?>) value; |
| |
| type = PROPERTY_TYPE_LIST; |
| String elType = resolvePropertyType(coll.iterator().next()); |
| if (elType != null) { |
| type += String.format("<%s>", elType); //$NON-NLS-1$ |
| } |
| |
| valueStr = coll.stream().map(Object::toString).collect(joining(",")); //$NON-NLS-1$ |
| } else { |
| type = resolvePropertyType(value); |
| valueStr = value.toString(); |
| } |
| |
| start(PROPERTY_ELEMENT); |
| attribute(PROPERTY_NAME_ATTRIBUTE, name); |
| attribute(PROPERTY_VALUE_ATTRIBUTE, valueStr); |
| attributeOptional(PROPERTY_TYPE_ATTRIBUTE, type); |
| end(); |
| } |
| |
| private String resolvePropertyType(Object value) { |
| if (value instanceof Integer) { |
| return PROPERTY_TYPE_INTEGER; |
| } |
| if (value instanceof Long) { |
| return PROPERTY_TYPE_LONG; |
| } |
| if (value instanceof Float) { |
| return PROPERTY_TYPE_FLOAT; |
| } |
| if (value instanceof Double) { |
| return PROPERTY_TYPE_DOUBLE; |
| } |
| if (value instanceof Byte) { |
| return PROPERTY_TYPE_BYTE; |
| } |
| if (value instanceof Short) { |
| return PROPERTY_TYPE_SHORT; |
| } |
| if (value instanceof Character) { |
| return PROPERTY_TYPE_CHARACTER; |
| } |
| if (value instanceof Boolean) { |
| return PROPERTY_TYPE_BOOLEAN; |
| } |
| if (value instanceof Version) { |
| return PROPERTY_TYPE_VERSION; |
| } |
| |
| // Null is read back as String |
| // NOTE: Using string as default is needed for backward compatibility with properties that are always String like |
| // the IU properties |
| return null; |
| } |
| |
| protected static String attributeImage(String name, String value) { |
| if (value == null) { |
| return ""; // optional attribute with no value //$NON-NLS-1$ |
| } |
| return name + "='" + escape(value) + '\''; //$NON-NLS-1$ |
| } |
| |
| private void println(char c) { |
| if (ignoreWhitespace) { |
| this.pw.print(c); |
| } else { |
| this.pw.println(c); |
| } |
| } |
| |
| private void println(String s) { |
| if (ignoreWhitespace) { |
| this.pw.print(s); |
| } else { |
| this.pw.println(s); |
| } |
| } |
| |
| private void println() { |
| if (!ignoreWhitespace) { |
| this.pw.println(); |
| } |
| } |
| |
| private void print(char c) { |
| this.pw.print(c); |
| } |
| |
| private void print(String s) { |
| this.pw.print(s); |
| } |
| |
| private void printlnIndented(String s, boolean escape) { |
| if (s.length() == 0) { |
| println(); |
| } else { |
| if (!ignoreWhitespace) { |
| indent(); |
| } |
| println(escape ? escape(s) : s); |
| } |
| } |
| |
| private void indent() { |
| if (!ignoreWhitespace) { |
| for (int i = this.elements.size(); i > 0; i -= 1) { |
| print(this.indent); |
| } |
| } |
| } |
| |
| public static class AttributeAfterNestedContentError extends Error { |
| private static final long serialVersionUID = 1L; // not serialized |
| } |
| |
| public static class EndWithoutStartError extends Error { |
| private static final long serialVersionUID = 1L; // not serialized |
| private String name; |
| |
| public EndWithoutStartError() { |
| super(); |
| } |
| |
| public EndWithoutStartError(String name) { |
| super(); |
| this.name = name; |
| } |
| |
| public String getName() { |
| return this.name; |
| } |
| } |
| |
| } |