|
| 1 | +//! Writers for Hadoop XML config files and Java `.properties` files. |
| 2 | +
|
| 3 | +use std::{fmt::Write as _, io::Write}; |
| 4 | + |
| 5 | +use java_properties::{PropertiesError, PropertiesWriter}; |
| 6 | +use snafu::{ResultExt, Snafu}; |
| 7 | +use xml::escape::escape_str_attribute; |
| 8 | + |
| 9 | +#[derive(Debug, Snafu)] |
| 10 | +pub enum PropertiesWriterError { |
| 11 | + #[snafu(display("failed to create properties file"))] |
| 12 | + Properties { source: PropertiesError }, |
| 13 | + |
| 14 | + #[snafu(display("failed to convert properties file byte array to UTF-8"))] |
| 15 | + FromUtf8 { source: std::string::FromUtf8Error }, |
| 16 | +} |
| 17 | + |
| 18 | +/// Creates a common Java properties file string in the format: |
| 19 | +/// `property_1=value_1\nproperty_2=value_2\n`. |
| 20 | +pub fn to_java_properties_string<'a, T>(properties: T) -> Result<String, PropertiesWriterError> |
| 21 | +where |
| 22 | + T: Iterator<Item = (&'a String, &'a Option<String>)>, |
| 23 | +{ |
| 24 | + let mut output = Vec::new(); |
| 25 | + write_java_properties(&mut output, properties)?; |
| 26 | + String::from_utf8(output).context(FromUtf8Snafu) |
| 27 | +} |
| 28 | + |
| 29 | +/// Writes Java properties to the given writer. A `None` value is written as an |
| 30 | +/// empty value (`key=`). |
| 31 | +fn write_java_properties<'a, W, T>(writer: W, properties: T) -> Result<(), PropertiesWriterError> |
| 32 | +where |
| 33 | + W: Write, |
| 34 | + T: Iterator<Item = (&'a String, &'a Option<String>)>, |
| 35 | +{ |
| 36 | + let mut writer = PropertiesWriter::new(writer); |
| 37 | + for (k, v) in properties { |
| 38 | + let property_value = v.as_deref().unwrap_or_default(); |
| 39 | + writer.write(k, property_value).context(PropertiesSnafu)?; |
| 40 | + } |
| 41 | + writer.flush().context(PropertiesSnafu)?; |
| 42 | + Ok(()) |
| 43 | +} |
| 44 | + |
| 45 | +/// Converts properties into a Hadoop configuration XML, including the wrapping |
| 46 | +/// `<configuration>...</configuration>` elements. Properties with a `None` value |
| 47 | +/// are skipped. Keys and values are XML-escaped. |
| 48 | +pub fn to_hadoop_xml<'a, T>(properties: T) -> String |
| 49 | +where |
| 50 | + T: Iterator<Item = (&'a String, &'a Option<String>)>, |
| 51 | +{ |
| 52 | + let mut snippet = String::new(); |
| 53 | + for (k, v) in properties { |
| 54 | + let escaped_value = match v { |
| 55 | + Some(value) => escape_str_attribute(value), |
| 56 | + None => continue, |
| 57 | + }; |
| 58 | + let escaped_key = escape_str_attribute(k); |
| 59 | + write!( |
| 60 | + snippet, |
| 61 | + " <property>\n <name>{escaped_key}</name>\n <value>{escaped_value}</value>\n </property>\n" |
| 62 | + ) |
| 63 | + .expect("writing to a String is infallible"); |
| 64 | + } |
| 65 | + format!("<?xml version=\"1.0\"?>\n<configuration>\n{snippet}</configuration>") |
| 66 | +} |
| 67 | + |
| 68 | +#[cfg(test)] |
| 69 | +mod tests { |
| 70 | + use std::collections::BTreeMap; |
| 71 | + |
| 72 | + use super::*; |
| 73 | + |
| 74 | + fn xml(pairs: &[(&str, Option<&str>)]) -> String { |
| 75 | + let map: BTreeMap<String, Option<String>> = pairs |
| 76 | + .iter() |
| 77 | + .map(|(k, v)| (k.to_string(), v.map(str::to_string))) |
| 78 | + .collect(); |
| 79 | + to_hadoop_xml(map.iter()) |
| 80 | + } |
| 81 | + |
| 82 | + fn props(pairs: &[(&str, Option<&str>)]) -> String { |
| 83 | + let map: BTreeMap<String, Option<String>> = pairs |
| 84 | + .iter() |
| 85 | + .map(|(k, v)| (k.to_string(), v.map(str::to_string))) |
| 86 | + .collect(); |
| 87 | + to_java_properties_string(map.iter()).unwrap() |
| 88 | + } |
| 89 | + |
| 90 | + #[test] |
| 91 | + fn hadoop_xml_wraps_empty_configuration() { |
| 92 | + assert_eq!( |
| 93 | + xml(&[]), |
| 94 | + "<?xml version=\"1.0\"?>\n<configuration>\n</configuration>" |
| 95 | + ); |
| 96 | + } |
| 97 | + |
| 98 | + #[test] |
| 99 | + fn hadoop_xml_renders_single_property() { |
| 100 | + assert_eq!( |
| 101 | + xml(&[("fs.defaultFS", Some("hdfs://hdfs/"))]), |
| 102 | + "<?xml version=\"1.0\"?>\n<configuration>\n \ |
| 103 | + <property>\n <name>fs.defaultFS</name>\n \ |
| 104 | + <value>hdfs://hdfs/</value>\n </property>\n</configuration>" |
| 105 | + ); |
| 106 | + } |
| 107 | + |
| 108 | + #[test] |
| 109 | + fn hadoop_xml_skips_none_values() { |
| 110 | + assert_eq!( |
| 111 | + xml(&[("kept", Some("1")), ("dropped", None)]), |
| 112 | + "<?xml version=\"1.0\"?>\n<configuration>\n \ |
| 113 | + <property>\n <name>kept</name>\n \ |
| 114 | + <value>1</value>\n </property>\n</configuration>" |
| 115 | + ); |
| 116 | + } |
| 117 | + |
| 118 | + #[test] |
| 119 | + fn hadoop_xml_escapes_special_characters() { |
| 120 | + let rendered = xml(&[("k", Some("<a>&b"))]); |
| 121 | + assert!( |
| 122 | + rendered.contains("<value><a>&b</value>"), |
| 123 | + "{rendered}" |
| 124 | + ); |
| 125 | + } |
| 126 | + |
| 127 | + #[test] |
| 128 | + fn java_properties_renders_key_value() { |
| 129 | + assert_eq!(props(&[("a", Some("1")), ("b", Some("2"))]), "a=1\nb=2\n"); |
| 130 | + } |
| 131 | + |
| 132 | + #[test] |
| 133 | + fn java_properties_renders_none_as_empty() { |
| 134 | + assert_eq!(props(&[("none", None)]), "none=\n"); |
| 135 | + } |
| 136 | + |
| 137 | + #[test] |
| 138 | + fn java_properties_escapes_colon_in_value() { |
| 139 | + assert_eq!( |
| 140 | + props(&[("url", Some("file://this/location/file.abc"))]), |
| 141 | + "url=file\\://this/location/file.abc\n" |
| 142 | + ); |
| 143 | + } |
| 144 | +} |
0 commit comments