| /* |
| * 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> |
| * <echoproperties /> |
| * </pre> |
| * Report the current properties to the log. |
| * <pre> |
| * <echoproperties destfile="my.properties" /> |
| * </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> |
| * <echoproperties destfile="my.properties" failonerror="false" |
| * prefix="ant" /> |
| * </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><echoproperties prefix="ant." /></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><echoproperties prefix=".*ant.*" /></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); |
| } |
| } |
| } |