SchemaTransformationUtils.java
/*******************************************************************************
* Copyright (c) 2021 Carsten Hammer.
*
* 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:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
/**
* Utilities for transforming XML/XSD/EXSD files using XSLT and safe
* formatting-only post-processing.
*/
public class SchemaTransformationUtils {
private static final Pattern MARKUP_INDENTATION= Pattern.compile("^( {4})+(?=<)", Pattern.MULTILINE); //$NON-NLS-1$
private SchemaTransformationUtils() {
}
/**
* Transform an XML file with default settings (no indentation).
*
* @param schemaPath path to the XML file
* @return transformed content
* @throws Exception if transformation fails
*/
public static String transform(Path schemaPath) throws Exception {
return transform(schemaPath, false);
}
/**
* Transform an XML file with configurable indentation. This compatibility
* overload assumes UTF-8; workspace callers use the content/charset overload.
*
* @param schemaPath path to the XML file
* @param enableIndent whether to enable indentation
* @return transformed content
* @throws Exception if transformation fails
*/
public static String transform(Path schemaPath, boolean enableIndent) throws Exception {
return transform(Files.readString(schemaPath, StandardCharsets.UTF_8),
StandardCharsets.UTF_8, enableIndent);
}
/**
* Transforms XML content without requiring a physical workspace location.
*
* @param sourceContent decoded XML source
* @param outputCharset charset named in the serialized XML declaration
* @param enableIndent whether to enable indentation
* @return transformed content
* @throws Exception if transformation fails
*/
public static String transform(String sourceContent, Charset outputCharset,
boolean enableIndent) throws Exception {
try (InputStream xslStream= SchemaTransformationUtils.class.getClassLoader()
.getResourceAsStream("resources/formatter.xsl")) { //$NON-NLS-1$
if (xslStream == null) {
throw new IllegalArgumentException("Unable to find formatter.xsl in resources."); //$NON-NLS-1$
}
TransformerFactory factory= TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); //$NON-NLS-1$
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); //$NON-NLS-1$
Transformer transformer= factory.newTransformer(new StreamSource(xslStream));
transformer.setOutputProperty(OutputKeys.INDENT, enableIndent ? "yes" : "no"); //$NON-NLS-1$ //$NON-NLS-2$
transformer.setOutputProperty(OutputKeys.ENCODING, outputCharset.name());
StringWriter output= new StringWriter(Math.max(1024, sourceContent.length()));
transformer.transform(new StreamSource(new StringReader(sourceContent)),
new StreamResult(output));
return convertMarkupIndentationToTabs(output.toString());
}
}
/**
* Converts groups of four leading spaces only when they indent serialized XML
* markup. Text lines are left unchanged, including intentional blank lines and
* leading spaces inside element content.
*/
private static String convertMarkupIndentationToTabs(String content) {
Matcher matcher= MARKUP_INDENTATION.matcher(content);
StringBuilder result= new StringBuilder(content.length());
while (matcher.find()) {
int tabs= matcher.group().length() / 4;
matcher.appendReplacement(result, "\t".repeat(tabs)); //$NON-NLS-1$
}
matcher.appendTail(result);
return result.toString();
}
}