blob: a31a7515dce22593ed1f2b3a19a33b01437f7ffd [file] [log] [blame]
/*******************************************************************************
* 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 = "&lt;"; //$NON-NLS-1$
break;
case '>' :
replace = "&gt;"; //$NON-NLS-1$
break;
case '"' :
replace = "&quot;"; //$NON-NLS-1$
break;
case '\'' :
replace = "&apos;"; //$NON-NLS-1$
break;
case '&' :
replace = "&amp;"; //$NON-NLS-1$
break;
case '\t' :
replace = "&#x9;"; //$NON-NLS-1$
break;
case '\n' :
replace = "&#xA;"; //$NON-NLS-1$
break;
case '\r' :
replace = "&#xD;"; //$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;
}
}
}