blob: 2eb91820df2fb87d9b62ea22e756c7855eea93fb [file] [log] [blame]
/*
* 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
*
* https://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.tools.ant.taskdefs.optional;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.PropertySet;
import org.apache.tools.ant.util.DOMElementWriter;
import org.apache.tools.ant.util.JavaEnvUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Displays all the current properties in the build. The output can be sent to
* a file if desired.
* <p>
* Attribute "destfile" defines a file to send the properties to. This can be
* processed as a standard property file later.
* </p>
* <p>
* Attribute "prefix" defines a prefix which is used to filter the properties
* only those properties starting with this prefix will be echoed.
* </p>
* <p>
* By default, the "failonerror" attribute is enabled. If an error occurs while
* writing the properties to a file, and this attribute is enabled, then a
* BuildException will be thrown. If disabled, then IO errors will be reported
* as a log statement, but no error will be thrown.
* </p>
* <p>Examples:</p><pre>
* &lt;echoproperties /&gt;
* </pre>
* Report the current properties to the log.
* <pre>
* &lt;echoproperties destfile="my.properties" /&gt;
* </pre>
* Report the current properties to the file "my.properties", and will
* fail the build if the file could not be created or written to.
* <pre>
* &lt;echoproperties destfile="my.properties" failonerror="false"
* prefix="ant" /&gt;
* </pre>
* Report all properties beginning with 'ant' to the file
* "my.properties", and will log a message if the file could not be created or
* written to, but will still allow the build to continue.
*
*@since Ant 1.5
*/
public class EchoProperties extends Task {
/**
* the properties element.
*/
private static final String PROPERTIES = "properties";
/**
* the property element.
*/
private static final String PROPERTY = "property";
/**
* name attribute for property, testcase and testsuite elements.
*/
private static final String ATTR_NAME = "name";
/**
* value attribute for property elements.
*/
private static final String ATTR_VALUE = "value";
/**
* the input file.
*/
private File inFile = null;
/**
* File object pointing to the output file. If this is null, then
* we output to the project log, not to a file.
*/
private File destfile = null;
/**
* If this is true, then errors generated during file output will become
* build errors, and if false, then such errors will be logged, but not
* thrown.
*/
private boolean failonerror = true;
private List<PropertySet> propertySets = new Vector<>();
private String format = "text";
private String prefix;
/**
* @since Ant 1.7
*/
private String regex;
/**
* Sets the input file.
*
* @param file the input file
*/
public void setSrcfile(File file) {
inFile = file;
}
/**
* Set a file to store the property output. If this is never specified,
* then the output will be sent to the Ant log.
*
* @param destfile file to store the property output
*/
public void setDestfile(File destfile) {
this.destfile = destfile;
}
/**
* If true, the task will fail if an error occurs writing the properties
* file, otherwise errors are just logged.
*
* @param failonerror <code>true</code> if IO exceptions are reported as build
* exceptions, or <code>false</code> if IO exceptions are ignored.
*/
public void setFailOnError(boolean failonerror) {
this.failonerror = failonerror;
}
/**
* If the prefix is set, then only properties which start with this
* prefix string will be recorded. If regex is not set and if this
* is never set, or it is set to an empty string or <code>null</code>,
* then all properties will be recorded.
*
* <p>For example, if the attribute is set as:</p>
* <pre>&lt;echoproperties prefix="ant." /&gt;</pre>
* then the property "ant.home" will be recorded, but "ant-example"
* will not.
*
* @param prefix The new prefix value
*/
public void setPrefix(String prefix) {
if (prefix != null && !prefix.isEmpty()) {
this.prefix = prefix;
PropertySet ps = new PropertySet();
ps.setProject(getProject());
ps.appendPrefix(prefix);
addPropertyset(ps);
}
}
/**
* If the regex is set, then only properties whose names match it
* will be recorded. If prefix is not set and if this is never set,
* or it is set to an empty string or <code>null</code>, then all
* properties will be recorded.
*
* <p>For example, if the attribute is set as:</p>
* <pre>&lt;echoproperties prefix=".*ant.*" /&gt;</pre>
* then the properties "ant.home" and "user.variant" will be recorded,
* but "ant-example" will not.
*
* @param regex The new regex value
*
* @since Ant 1.7
*/
public void setRegex(String regex) {
if (regex != null && !regex.isEmpty()) {
this.regex = regex;
PropertySet ps = new PropertySet();
ps.setProject(getProject());
ps.appendRegex(regex);
addPropertyset(ps);
}
}
/**
* A set of properties to write.
* @param ps the property set to write
* @since Ant 1.6
*/
public void addPropertyset(PropertySet ps) {
propertySets.add(ps);
}
/**
* Set the output format - xml or text.
* @param ea an enumerated <code>FormatAttribute</code> value
*/
public void setFormat(FormatAttribute ea) {
format = ea.getValue();
}
/**
* A enumerated type for the format attribute.
* The values are "xml" and "text".
*/
public static class FormatAttribute extends EnumeratedAttribute {
private String[] formats = new String[] {"xml", "text"};
/**
* @see EnumeratedAttribute#getValues()
* @return accepted values
*/
@Override
public String[] getValues() {
return formats;
}
}
/**
* Run the task.
*
* @exception BuildException trouble, probably file IO
*/
@Override
public void execute() throws BuildException {
if (prefix != null && regex != null) {
throw new BuildException(
"Please specify either prefix or regex, but not both",
getLocation());
}
//copy the properties file
Hashtable<Object, Object> allProps = new Hashtable<>();
/* load properties from file if specified, otherwise
use Ant's properties */
if (inFile == null && propertySets.isEmpty()) {
// add ant properties
allProps.putAll(getProject().getProperties());
} else if (inFile != null) {
if (inFile.isDirectory()) {
String message = "srcfile is a directory!";
if (failonerror) {
throw new BuildException(message, getLocation());
}
log(message, Project.MSG_ERR);
return;
}
if (inFile.exists() && !inFile.canRead()) {
String message = "Can not read from the specified srcfile!";
if (failonerror) {
throw new BuildException(message, getLocation());
} else {
log(message, Project.MSG_ERR);
}
return;
}
try (InputStream in = Files.newInputStream(inFile.toPath())) {
Properties props = new Properties();
props.load(in);
allProps.putAll(props);
} catch (FileNotFoundException fnfe) {
String message =
"Could not find file " + inFile.getAbsolutePath();
if (failonerror) {
throw new BuildException(message, fnfe, getLocation());
}
log(message, Project.MSG_WARN);
return;
} catch (IOException ioe) {
String message =
"Could not read file " + inFile.getAbsolutePath();
if (failonerror) {
throw new BuildException(message, ioe, getLocation());
}
log(message, Project.MSG_WARN);
return;
}
}
propertySets.stream().map(PropertySet::getProperties)
.forEach(allProps::putAll);
try (OutputStream os = createOutputStream()) {
if (os != null) {
saveProperties(allProps, os);
}
} catch (IOException ioe) {
if (failonerror) {
throw new BuildException(ioe, getLocation());
}
log(ioe.getMessage(), Project.MSG_INFO);
}
}
/**
* Send the key/value pairs in the hashtable to the given output stream.
* Only those properties matching the <code>prefix</code> constraint will be
* sent to the output stream.
* The output stream will be closed when this method returns.
*
* @param allProps propfile to save
* @param os output stream
* @throws IOException on output errors
* @throws BuildException on other errors
*/
protected void saveProperties(Hashtable<Object, Object> allProps, OutputStream os)
throws IOException, BuildException {
final List<Object> keyList = new ArrayList<>(allProps.keySet());
Properties props = new Properties() {
private static final long serialVersionUID = 5090936442309201654L;
@Override
public Enumeration<Object> keys() {
return keyList.stream()
.sorted(Comparator.comparing(Object::toString))
.collect(Collectors.collectingAndThen(Collectors.toList(),
Collections::enumeration));
}
@Override
public Set<Map.Entry<Object, Object>> entrySet() {
Set<Map.Entry<Object, Object>> t =
new TreeSet<>(Comparator.comparing(
((Function<Map.Entry<Object, Object>, Object>) Map.Entry::getKey)
.andThen(Object::toString)));
t.addAll(super.entrySet());
return t;
}
};
allProps.forEach((k, v) -> props.put(String.valueOf(k), String.valueOf(v)));
if ("text".equals(format)) {
jdkSaveProperties(props, os, "Ant properties");
} else if ("xml".equals(format)) {
xmlSaveProperties(props, os);
}
}
/**
* a tuple for the sort list.
*/
private static final class Tuple implements Comparable<Tuple> {
private String key;
private String value;
private Tuple(String key, String value) {
this.key = key;
this.value = value;
}
/**
* Compares this object with the specified object for order.
* @param o the Object to be compared.
* @return a negative integer, zero, or a positive integer as this object is
* less than, equal to, or greater than the specified object.
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this Object.
*/
@Override
public int compareTo(Tuple o) {
return Comparator.<String> naturalOrder().compare(key, o.key);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || o.getClass() != getClass()) {
return false;
}
Tuple that = (Tuple) o;
return Objects.equals(key, that.key)
&& Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(key);
}
}
private List<Tuple> sortProperties(Properties props) {
// sort the list. Makes SCM and manual diffs easier.
return props.stringPropertyNames().stream()
.map(k -> new Tuple(k, props.getProperty(k))).sorted()
.collect(Collectors.toList());
}
/**
* Output the properties as xml output.
* @param props the properties to save
* @param os the output stream to write to (Note this gets closed)
* @throws IOException on error in writing to the stream
*/
protected void xmlSaveProperties(Properties props,
OutputStream os) throws IOException {
// create XML document
Document doc = getDocumentBuilder().newDocument();
Element rootElement = doc.createElement(PROPERTIES);
List<Tuple> sorted = sortProperties(props);
// output properties
for (Tuple tuple : sorted) {
Element propElement = doc.createElement(PROPERTY);
propElement.setAttribute(ATTR_NAME, tuple.key);
propElement.setAttribute(ATTR_VALUE, tuple.value);
rootElement.appendChild(propElement);
}
try (Writer wri = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
wri.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
new DOMElementWriter().write(rootElement, wri, 0, "\t");
wri.flush();
} catch (IOException ioe) {
throw new BuildException("Unable to write XML file", ioe);
}
}
/**
* JDK 1.2 allows for the safer method
* <code>Properties.store(OutputStream, String)</code>, which throws an
* <code>IOException</code> on an output error.
*
* @param props the properties to record
* @param os record the properties to this output stream
* @param header prepend this header to the property output
* @exception IOException on an I/O error during a write.
*/
protected void jdkSaveProperties(Properties props, OutputStream os,
String header) throws IOException {
try {
props.store(os, header);
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
} finally {
if (os != null) {
try {
os.close();
} catch (IOException ioex) {
log("Failed to close output stream");
}
}
}
}
private OutputStream createOutputStream() throws IOException {
if (destfile == null) {
return new LogOutputStream(this);
}
if (destfile.exists() && destfile.isDirectory()) {
String message = "destfile is a directory!";
if (failonerror) {
throw new BuildException(message, getLocation());
}
log(message, Project.MSG_ERR);
return null;
}
if (destfile.exists() && !destfile.canWrite()) {
String message =
"Can not write to the specified destfile!";
if (failonerror) {
throw new BuildException(message, getLocation());
}
log(message, Project.MSG_ERR);
return null;
}
return Files.newOutputStream(this.destfile.toPath());
}
/**
* Uses the DocumentBuilderFactory to get a DocumentBuilder instance.
*
* @return The DocumentBuilder instance
*/
private static DocumentBuilder getDocumentBuilder() {
try {
return DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
}