/*
* Copyright 2016 DiffPlug
*
* Licensed 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 com.diffplug.spotless;
import static com.diffplug.spotless.MoreIterables.toNullHostileList;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/** Utility manages settings of formatter configured by properties. */
public final class FormatterProperties {
private final Properties properties;
private FormatterProperties() {
properties = new Properties();
}
/**
* Import settings from a sequence of files (file import is the given order)
*
* @param files
* Sequence of files
* @throws IllegalArgumentException
* In case the import of a file fails
*/
public static FormatterProperties from(File... files) throws IllegalArgumentException {
Objects.requireNonNull(files);
return from(Arrays.asList(files));
}
/**
* Import settings from a sequence of files (file import is the given order)
*
* @param files
* Sequence of files
* @throws IllegalArgumentException
* In case the import of a file fails
*/
public static FormatterProperties from(Iterable<File> files) throws IllegalArgumentException {
List<File> nonNullFiles = toNullHostileList(files);
FormatterProperties properties = new FormatterProperties();
nonNullFiles.forEach(properties::add);
return properties;
}
/**
* Import settings from given file. New settings (with the same ID/key)
* override existing once.
*
* @param settingsFile
* File
* @throws IllegalArgumentException
* In case the import of the file fails
*/
private void add(final File settingsFile) throws IllegalArgumentException {
Objects.requireNonNull(settingsFile);
if (!(settingsFile.isFile() || settingsFile.canRead())) {
String msg = String.format("Settings file '%s' does not exist or can not be read.", settingsFile);
throw new IllegalArgumentException(msg);
}
try {
Properties newSettings = FileParser.parse(settingsFile);
properties.putAll(newSettings);
} catch (IOException | IllegalArgumentException | NullPointerException exception) {
String message = String.format("Failed to add properties from '%s' to formatter settings.", settingsFile);
String detailedMessage = exception.getMessage();
if (null != detailedMessage) {
message += String.format(" %s", detailedMessage);
}
throw new IllegalArgumentException(message, exception);
}
}
/** Returns the accumulated {@link java.util.Properties Properties} */
public Properties getProperties() {
return properties;
}
private enum FileParser {
LINE_ORIENTED("properties", "prefs") {
@Override
protected Properties execute(File file) throws IOException, IllegalArgumentException {
Properties properties = new Properties();
try (InputStream inputProperties = new FileInputStream(file)) {
properties.load(inputProperties);
}
return properties;
}
},
XML("xml") {
@Override
protected Properties execute(final File file) throws IOException, IllegalArgumentException {
Node rootNode = getRootNode(file);
String nodeName = rootNode.getNodeName();
if (null == nodeName) {
throw new IllegalArgumentException("XML document does not contain a root node.");
}
return XmlParser.parse(file, rootNode);
}
private Node getRootNode(final File file) throws IOException, IllegalArgumentException {
try {
/*
* The parser does not validate, since the root node is only
* used to decide on further processing.
*/
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(file).getDocumentElement();
} catch (SAXException | ParserConfigurationException e) {
throw new IllegalArgumentException("File has no valid XML syntax.", e);
}
}
};
private static final char FILE_EXTENSION_SEPARATOR = '.';
private final List<String> supportedFileNameExtensions;
FileParser(final String... supportedFileNameExtensions) {
this.supportedFileNameExtensions = Arrays.asList(supportedFileNameExtensions);
}
protected abstract Properties execute(File file) throws IOException, IllegalArgumentException;
public static Properties parse(final File file) throws IOException, IllegalArgumentException {
String fileNameExtension = getFileNameExtension(file);
for (FileParser parser : FileParser.values()) {
if (parser.supportedFileNameExtensions.contains(fileNameExtension)) {
return parser.execute(file);
}
}
String msg = String.format(
"The file name extension '%1$s' is not part of the supported file extensions [%2$s].",
fileNameExtension, Arrays.toString(FileParser.values()));
throw new IllegalArgumentException(msg);
}
private static String getFileNameExtension(File file) {
String fileName = file.getName();
int seperatorPos = fileName.lastIndexOf(FILE_EXTENSION_SEPARATOR);
return 0 > seperatorPos ? "" : fileName.substring(seperatorPos + 1);
}
}
private enum XmlParser {
PROPERTIES("properties") {
@Override
protected Properties execute(final File xmlFile, final Node rootNode)
throws IOException, IllegalArgumentException {
final Properties properties = new Properties();
try (InputStream xmlInput = new FileInputStream(xmlFile)) {
properties.loadFromXML(xmlInput);
}
return properties;
}
},
PROFILES("profiles") {
@Override
protected Properties execute(File file, Node rootNode) throws IOException, IllegalArgumentException {
final Properties properties = new Properties();
Node firstProfile = getSingleProfile(rootNode);
for (Object settingObj : getChildren(firstProfile, "setting")) {
Node setting = (Node) settingObj;
NamedNodeMap attributes = setting.getAttributes();
Node id = attributes.getNamedItem("id");
Node value = attributes.getNamedItem("value");
if (null == id) {
throw new IllegalArgumentException("Node 'setting' does not possess an 'id' attribute.");
}
String idString = id.getNodeValue();
/*
* A missing value is interpreted as an empty string,
* similar to the Properties behavior
*/
String valString = (null == value) ? "" : value.getNodeValue();
properties.setProperty(idString, valString);
}
return properties;
}
private Node getSingleProfile(final Node rootNode) throws IllegalArgumentException {
List<Node> profiles = getChildren(rootNode, "profile");
if (profiles.isEmpty()) {
throw new IllegalArgumentException("The formatter configuration profile files does not contain any 'profile' elements.");
}
if (profiles.size() > 1) {
String message = "Formatter configuration file contains multiple profiles: [";
message += profiles.stream().map(XmlParser::getProfileName).collect(Collectors.joining("; "));
message += "]%n The formatter can only cope with a single profile per configuration file. Please remove the other profiles.";
throw new IllegalArgumentException(message);
}
return profiles.iterator().next();
}
private List<Node> getChildren(final Node node, final String nodeName) {
NodeList children = node.getChildNodes();
return IntStream.range(0, children.getLength()) //
.mapToObj(children::item) //
.filter(child -> child.getNodeName().equals(nodeName)) //
.collect(Collectors.toCollection(LinkedList::new));
}
};
private static String getProfileName(Node profile) {
Node nameAttribute = profile.getAttributes().getNamedItem("name");
return (null == nameAttribute) ? "" : nameAttribute.getNodeValue();
}
private final String rootNodeName;
XmlParser(final String rootNodeName) {
this.rootNodeName = rootNodeName;
}
@Override
public String toString() {
return this.rootNodeName;
}
protected abstract Properties execute(File file, Node rootNode) throws IOException, IllegalArgumentException;
public static Properties parse(final File file, final Node rootNode)
throws IOException, IllegalArgumentException {
String rootNodeName = rootNode.getNodeName();
for (XmlParser parser : XmlParser.values()) {
if (parser.rootNodeName.equals(rootNodeName)) {
return parser.execute(file, rootNode);
}
}
String msg = String.format("The XML root node '%1$s' is not part of the supported root nodes [%2$s].",
rootNodeName, Arrays.toString(XmlParser.values()));
throw new IllegalArgumentException(msg);
}
}
}