| 33 | 33 | import com.fasterxml.jackson.databind.JsonNode; |
| 34 | 34 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 35 | | import com.fasterxml.jackson.databind.node.ObjectNode; |
| 36 | | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; |
| 37 | | import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; |
| 38 | | import com.scrivenvar.decorators.VariableDecorator; |
| 39 | | import com.scrivenvar.decorators.YamlVariableDecorator; |
| 40 | | import java.io.IOException; |
| 41 | | import java.io.InputStream; |
| 42 | | import java.io.Writer; |
| 43 | | import java.text.MessageFormat; |
| 44 | | import java.util.HashMap; |
| 45 | | import java.util.Map; |
| 46 | | import java.util.Map.Entry; |
| 47 | | import java.util.regex.Matcher; |
| 48 | | import java.util.regex.Pattern; |
| 49 | | import org.yaml.snakeyaml.DumperOptions; |
| 50 | | |
| 51 | | /** |
| 52 | | * <p> |
| 53 | | * This program loads a YAML document into memory, scans for variable |
| 54 | | * declarations, then substitutes any self-referential values back into the |
| 55 | | * document. Its output is the given YAML document without any variables. |
| 56 | | * Variables in the YAML document are denoted using a bracketed dollar symbol |
| 57 | | * syntax. For example: $field.name$. Some nomenclature to keep from going |
| 58 | | * squirrely, consider: |
| 59 | | * </p> |
| 60 | | * |
| 61 | | * <pre> |
| 62 | | * root: |
| 63 | | * node: |
| 64 | | * name: $field.name$ |
| 65 | | * field: |
| 66 | | * name: Alan Turing |
| 67 | | * </pre> |
| 68 | | * |
| 69 | | * The various components of the given YAML are called: |
| 70 | | * |
| 71 | | * <ul> |
| 72 | | * <li><code>$field.name$</code> - delimited reference</li> |
| 73 | | * <li><code>field.name</code> - reference</li> |
| 74 | | * <li><code>name</code> - YAML field</li> |
| 75 | | * <li><code>Alan Turing</code> - (dereferenced) field value</li> |
| 76 | | * </ul> |
| 77 | | * |
| 78 | | * @author White Magic Software, Ltd. |
| 79 | | */ |
| 80 | | public class YamlParser { |
| 81 | | |
| 82 | | /** |
| 83 | | * Separates YAML variable nodes (e.g., the dots in |
| 84 | | * <code>$root.node.var$</code>). |
| 85 | | */ |
| 86 | | public static final String SEPARATOR = "."; |
| 87 | | public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 ); |
| 88 | | |
| 89 | | private final static int GROUP_DELIMITED = 1; |
| 90 | | private final static int GROUP_REFERENCE = 2; |
| 91 | | |
| 92 | | private final static VariableDecorator VARIABLE_DECORATOR |
| 93 | | = new YamlVariableDecorator(); |
| 94 | | |
| 95 | | private String error; |
| 96 | | |
| 97 | | /** |
| 98 | | * Compiled version of DEFAULT_REGEX. |
| 99 | | */ |
| 100 | | private final static Pattern REGEX_PATTERN |
| 101 | | = Pattern.compile( YamlVariableDecorator.REGEX ); |
| 102 | | |
| 103 | | /** |
| 104 | | * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values. |
| 105 | | */ |
| 106 | | private final static char SEPARATOR_YAML = '/'; |
| 107 | | |
| 108 | | /** |
| 109 | | * Start of the Universe (the YAML document node that contains all others). |
| 110 | | */ |
| 111 | | private JsonNode documentRoot; |
| 112 | | |
| 113 | | /** |
| 114 | | * Map of references to dereferenced field values. |
| 115 | | */ |
| 116 | | private Map<String, String> references; |
| 117 | | |
| 118 | | public YamlParser( final InputStream in ) throws IOException { |
| 119 | | process( in ); |
| 120 | | } |
| 121 | | |
| 122 | | /** |
| 123 | | * Returns the given string with all the delimited references swapped with |
| 124 | | * their recursively resolved values. |
| 125 | | * |
| 126 | | * @param text The text to parse with zero or more delimited references to |
| 127 | | * replace. |
| 128 | | * |
| 129 | | * @return The substituted value. |
| 130 | | */ |
| 131 | | public String substitute( String text ) { |
| 132 | | final Matcher matcher = patternMatch( text ); |
| 133 | | final Map<String, String> map = getReferences(); |
| 134 | | |
| 135 | | while( matcher.find() ) { |
| 136 | | final String key = matcher.group( GROUP_DELIMITED ); |
| 137 | | final String value = map.get( key ); |
| 138 | | |
| 139 | | if( value == null ) { |
| 140 | | missing( text ); |
| 141 | | } |
| 142 | | else { |
| 143 | | text = text.replace( key, value ); |
| 144 | | } |
| 145 | | } |
| 146 | | |
| 147 | | return text; |
| 148 | | } |
| 149 | | |
| 150 | | /** |
| 151 | | * Returns all the strings with their values resolved in a flat hierarchy. |
| 152 | | * This copies all the keys and resolved values into a new map. |
| 153 | | * |
| 154 | | * @return The new map created with all values having been resolved, |
| 155 | | * recursively. |
| 156 | | */ |
| 157 | | public Map<String, String> createResolvedMap() { |
| 158 | | final Map<String, String> map = new HashMap<>( 1024 ); |
| 159 | | |
| 160 | | resolve( getDocumentRoot(), "", map ); |
| 161 | | |
| 162 | | return map; |
| 163 | | } |
| 164 | | |
| 165 | | /** |
| 166 | | * Iterate over a given root node (at any level of the tree) and adapt each |
| 167 | | * leaf node. |
| 168 | | * |
| 169 | | * @param rootNode A JSON node (YAML node) to adapt. |
| 170 | | * @param map Container that associates definitions with values. |
| 171 | | */ |
| 172 | | private void resolve( |
| 173 | | final JsonNode rootNode, |
| 174 | | final String path, |
| 175 | | final Map<String, String> map ) { |
| 176 | | |
| 177 | | if( rootNode != null ) { |
| 178 | | rootNode.fields().forEachRemaining( |
| 179 | | (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map ) |
| 180 | | ); |
| 181 | | } |
| 182 | | } |
| 183 | | |
| 184 | | /** |
| 185 | | * Recursively adapt each rootNode to a corresponding rootItem. |
| 186 | | * |
| 187 | | * @param rootNode The node to adapt. |
| 188 | | */ |
| 189 | | private void resolve( |
| 190 | | final Entry<String, JsonNode> rootNode, |
| 191 | | final String path, |
| 192 | | final Map<String, String> map ) { |
| 193 | | |
| 194 | | final JsonNode leafNode = rootNode.getValue(); |
| 195 | | final String key = rootNode.getKey(); |
| 196 | | |
| 197 | | if( leafNode.isValueNode() ) { |
| 198 | | final String value = rootNode.getValue().asText(); |
| 199 | | |
| 200 | | map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) ); |
| 201 | | } |
| 202 | | |
| 203 | | if( leafNode.isObject() ) { |
| 204 | | resolve( leafNode, path + key + SEPARATOR, map ); |
| 205 | | } |
| 206 | | } |
| 207 | | |
| 208 | | /** |
| 209 | | * Reads the first document from the given stream of YAML data and returns a |
| 210 | | * corresponding object that represents the YAML hierarchy. The calling class |
| 211 | | * is responsible for closing the stream. Calling classes should use |
| 212 | | * <code>JsonNode.fields()</code> to walk through the YAML tree of fields. |
| 213 | | * |
| 214 | | * @param in The input stream containing YAML content. |
| 215 | | * |
| 216 | | * @return An object hierarchy to represent the content. |
| 217 | | * |
| 218 | | * @throws IOException Could not read the stream. |
| 219 | | */ |
| 220 | | private JsonNode process( final InputStream in ) throws IOException { |
| 221 | | final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in ); |
| 222 | | setDocumentRoot( root ); |
| 223 | | process( root ); |
| 224 | | return getDocumentRoot(); |
| 225 | | } |
| 226 | | |
| 227 | | /** |
| 228 | | * Iterate over a given root node (at any level of the tree) and process each |
| 229 | | * leaf node. |
| 230 | | * |
| 231 | | * @param root A node to process. |
| 232 | | */ |
| 233 | | private void process( final JsonNode root ) { |
| 234 | | root.fields().forEachRemaining( this::process ); |
| 235 | | } |
| 236 | | |
| 237 | | /** |
| 238 | | * Process the given field, which is a named node. This is where the |
| 239 | | * application does the up-front work of mapping references to their fully |
| 240 | | * recursively dereferenced values. |
| 241 | | * |
| 242 | | * @param field The named node. |
| 243 | | */ |
| 244 | | private void process( final Entry<String, JsonNode> field ) { |
| 245 | | final JsonNode node = field.getValue(); |
| 246 | | |
| 247 | | if( node.isObject() ) { |
| 248 | | process( node ); |
| 249 | | } |
| 250 | | else { |
| 251 | | final JsonNode fieldValue = field.getValue(); |
| 252 | | |
| 253 | | // Only basic data types can be parsed into variable values. For |
| 254 | | // node structures, YAML has a built-in mechanism. |
| 255 | | if( fieldValue.isValueNode() ) { |
| 256 | | try { |
| 257 | | resolve( fieldValue.asText() ); |
| 258 | | } catch( StackOverflowError e ) { |
| 259 | | setError( "Unresolvable: " + node.textValue() + " = " + fieldValue ); |
| 260 | | } |
| 261 | | } |
| 262 | | } |
| 263 | | } |
| 264 | | |
| 265 | | /** |
| 266 | | * Inserts the delimited references and field values into the cache. This will |
| 267 | | * overwrite existing references. |
| 268 | | * |
| 269 | | * @param fieldValue YAML field containing zero or more delimited references. |
| 270 | | * If it contains a delimited reference, the parameter is modified with the |
| 271 | | * dereferenced value before it is returned. |
| 272 | | * |
| 273 | | * @return fieldValue without delimited references. |
| 274 | | */ |
| 275 | | private String resolve( String fieldValue ) { |
| 276 | | final Matcher matcher = patternMatch( fieldValue ); |
| 277 | | |
| 278 | | while( matcher.find() ) { |
| 279 | | final String delimited = matcher.group( GROUP_DELIMITED ); |
| 280 | | final String reference = matcher.group( GROUP_REFERENCE ); |
| 281 | | final String dereference = resolve( lookup( reference ) ); |
| 282 | | |
| 283 | | fieldValue = fieldValue.replace( delimited, dereference ); |
| 284 | | |
| 285 | | // This will perform some superfluous calls by overwriting existing |
| 286 | | // items in the delimited reference map. |
| 287 | | put( delimited, dereference ); |
| 288 | | } |
| 289 | | |
| 290 | | return fieldValue; |
| 291 | | } |
| 292 | | |
| 293 | | /** |
| 294 | | * Inserts a key/value pair into the references map. The map retains |
| 295 | | * references and dereferenced values found in the YAML. If the reference |
| 296 | | * already exists, this will overwrite with a new value. |
| 297 | | * |
| 298 | | * @param delimited The variable name. |
| 299 | | * @param dereferenced The resolved value. |
| 300 | | */ |
| 301 | | private void put( String delimited, String dereferenced ) { |
| 302 | | if( dereferenced.isEmpty() ) { |
| 303 | | missing( delimited ); |
| 304 | | } |
| 305 | | else { |
| 306 | | getReferences().put( delimited, dereferenced ); |
| 307 | | } |
| 308 | | } |
| 309 | | |
| 310 | | /** |
| 311 | | * Writes the modified YAML document to standard output. |
| 312 | | */ |
| 313 | | private void writeDocument() throws IOException { |
| 314 | | getObjectMapper().writeValue( System.out, getDocumentRoot() ); |
| 315 | | } |
| 316 | | |
| 317 | | /** |
| 318 | | * Called when a delimited reference is dereferenced to an empty string. This |
| 319 | | * should produce a warning for the user. |
| 320 | | * |
| 321 | | * @param delimited Delimited reference with no derived value. |
| 322 | | */ |
| 323 | | private void missing( final String delimited ) { |
| 324 | | setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) ); |
| 325 | | } |
| 326 | | |
| 327 | | /** |
| 328 | | * Returns a REGEX_PATTERN matcher for the given text. |
| 329 | | * |
| 330 | | * @param text The text that contains zero or more instances of a |
| 331 | | * REGEX_PATTERN that can be found using the regular expression. |
| 332 | | */ |
| 333 | | private Matcher patternMatch( String text ) { |
| 334 | | return getPattern().matcher( text ); |
| 335 | | } |
| 336 | | |
| 337 | | /** |
| 338 | | * Finds the YAML value for a reference. |
| 339 | | * |
| 340 | | * @param reference References a value in the YAML document. |
| 341 | | * |
| 342 | | * @return The dereferenced value. |
| 343 | | */ |
| 344 | | private String lookup( final String reference ) { |
| 345 | | return getDocumentRoot().at( asPath( reference ) ).asText(); |
| 346 | | } |
| 347 | | |
| 348 | | /** |
| 349 | | * Converts a reference (not delimited) to a path that can be used to find a |
| 350 | | * value that should exist inside the YAML document. |
| 351 | | * |
| 352 | | * @param reference The reference to convert to a YAML document path. |
| 353 | | * |
| 354 | | * @return The reference with a leading slash and its separator characters |
| 355 | | * converted to slashes. |
| 356 | | */ |
| 357 | | private String asPath( final String reference ) { |
| 358 | | return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML ); |
| 359 | | } |
| 360 | | |
| 361 | | /** |
| 362 | | * Sets the parent node for the entire YAML document tree. |
| 363 | | * |
| 364 | | * @param documentRoot The parent node. |
| 365 | | */ |
| 366 | | private void setDocumentRoot( ObjectNode documentRoot ) { |
| 35 | import com.fasterxml.jackson.databind.node.NullNode; |
| 36 | import com.fasterxml.jackson.databind.node.ObjectNode; |
| 37 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; |
| 38 | import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; |
| 39 | import com.scrivenvar.decorators.VariableDecorator; |
| 40 | import com.scrivenvar.decorators.YamlVariableDecorator; |
| 41 | import java.io.IOException; |
| 42 | import java.io.InputStream; |
| 43 | import java.io.Writer; |
| 44 | import java.text.MessageFormat; |
| 45 | import java.util.HashMap; |
| 46 | import java.util.Map; |
| 47 | import java.util.Map.Entry; |
| 48 | import java.util.regex.Matcher; |
| 49 | import java.util.regex.Pattern; |
| 50 | import org.yaml.snakeyaml.DumperOptions; |
| 51 | |
| 52 | /** |
| 53 | * <p> |
| 54 | * This program loads a YAML document into memory, scans for variable |
| 55 | * declarations, then substitutes any self-referential values back into the |
| 56 | * document. Its output is the given YAML document without any variables. |
| 57 | * Variables in the YAML document are denoted using a bracketed dollar symbol |
| 58 | * syntax. For example: $field.name$. Some nomenclature to keep from going |
| 59 | * squirrely, consider: |
| 60 | * </p> |
| 61 | * |
| 62 | * <pre> |
| 63 | * root: |
| 64 | * node: |
| 65 | * name: $field.name$ |
| 66 | * field: |
| 67 | * name: Alan Turing |
| 68 | * </pre> |
| 69 | * |
| 70 | * The various components of the given YAML are called: |
| 71 | * |
| 72 | * <ul> |
| 73 | * <li><code>$field.name$</code> - delimited reference</li> |
| 74 | * <li><code>field.name</code> - reference</li> |
| 75 | * <li><code>name</code> - YAML field</li> |
| 76 | * <li><code>Alan Turing</code> - (dereferenced) field value</li> |
| 77 | * </ul> |
| 78 | * |
| 79 | * @author White Magic Software, Ltd. |
| 80 | */ |
| 81 | public class YamlParser { |
| 82 | |
| 83 | /** |
| 84 | * Separates YAML variable nodes (e.g., the dots in |
| 85 | * <code>$root.node.var$</code>). |
| 86 | */ |
| 87 | public static final String SEPARATOR = "."; |
| 88 | public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 ); |
| 89 | |
| 90 | private final static int GROUP_DELIMITED = 1; |
| 91 | private final static int GROUP_REFERENCE = 2; |
| 92 | |
| 93 | private final static VariableDecorator VARIABLE_DECORATOR |
| 94 | = new YamlVariableDecorator(); |
| 95 | |
| 96 | private String error; |
| 97 | |
| 98 | /** |
| 99 | * Compiled version of DEFAULT_REGEX. |
| 100 | */ |
| 101 | private final static Pattern REGEX_PATTERN |
| 102 | = Pattern.compile( YamlVariableDecorator.REGEX ); |
| 103 | |
| 104 | /** |
| 105 | * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values. |
| 106 | */ |
| 107 | private final static char SEPARATOR_YAML = '/'; |
| 108 | |
| 109 | /** |
| 110 | * Start of the Universe (the YAML document node that contains all others). |
| 111 | */ |
| 112 | private JsonNode documentRoot; |
| 113 | |
| 114 | /** |
| 115 | * Map of references to dereferenced field values. |
| 116 | */ |
| 117 | private Map<String, String> references; |
| 118 | |
| 119 | public YamlParser( final InputStream in ) throws IOException { |
| 120 | process( in ); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Returns the given string with all the delimited references swapped with |
| 125 | * their recursively resolved values. |
| 126 | * |
| 127 | * @param text The text to parse with zero or more delimited references to |
| 128 | * replace. |
| 129 | * |
| 130 | * @return The substituted value. |
| 131 | */ |
| 132 | public String substitute( String text ) { |
| 133 | final Matcher matcher = patternMatch( text ); |
| 134 | final Map<String, String> map = getReferences(); |
| 135 | |
| 136 | while( matcher.find() ) { |
| 137 | final String key = matcher.group( GROUP_DELIMITED ); |
| 138 | final String value = map.get( key ); |
| 139 | |
| 140 | if( value == null ) { |
| 141 | missing( text ); |
| 142 | } |
| 143 | else { |
| 144 | text = text.replace( key, value ); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | return text; |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Returns all the strings with their values resolved in a flat hierarchy. |
| 153 | * This copies all the keys and resolved values into a new map. |
| 154 | * |
| 155 | * @return The new map created with all values having been resolved, |
| 156 | * recursively. |
| 157 | */ |
| 158 | public Map<String, String> createResolvedMap() { |
| 159 | final Map<String, String> map = new HashMap<>( 1024 ); |
| 160 | |
| 161 | resolve( getDocumentRoot(), "", map ); |
| 162 | |
| 163 | return map; |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * Iterate over a given root node (at any level of the tree) and adapt each |
| 168 | * leaf node. |
| 169 | * |
| 170 | * @param rootNode A JSON node (YAML node) to adapt. |
| 171 | * @param map Container that associates definitions with values. |
| 172 | */ |
| 173 | private void resolve( |
| 174 | final JsonNode rootNode, |
| 175 | final String path, |
| 176 | final Map<String, String> map ) { |
| 177 | |
| 178 | if( rootNode != null ) { |
| 179 | rootNode.fields().forEachRemaining( |
| 180 | (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map ) |
| 181 | ); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * Recursively adapt each rootNode to a corresponding rootItem. |
| 187 | * |
| 188 | * @param rootNode The node to adapt. |
| 189 | */ |
| 190 | private void resolve( |
| 191 | final Entry<String, JsonNode> rootNode, |
| 192 | final String path, |
| 193 | final Map<String, String> map ) { |
| 194 | |
| 195 | final JsonNode leafNode = rootNode.getValue(); |
| 196 | final String key = rootNode.getKey(); |
| 197 | |
| 198 | |
| 199 | if( leafNode.isValueNode() ) { |
| 200 | final String value; |
| 201 | |
| 202 | if( leafNode instanceof NullNode ) { |
| 203 | value = ""; |
| 204 | } |
| 205 | else { |
| 206 | value = rootNode.getValue().asText(); |
| 207 | } |
| 208 | |
| 209 | map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) ); |
| 210 | } |
| 211 | |
| 212 | if( leafNode.isObject() ) { |
| 213 | resolve( leafNode, path + key + SEPARATOR, map ); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Reads the first document from the given stream of YAML data and returns a |
| 219 | * corresponding object that represents the YAML hierarchy. The calling class |
| 220 | * is responsible for closing the stream. Calling classes should use |
| 221 | * <code>JsonNode.fields()</code> to walk through the YAML tree of fields. |
| 222 | * |
| 223 | * @param in The input stream containing YAML content. |
| 224 | * |
| 225 | * @return An object hierarchy to represent the content. |
| 226 | * |
| 227 | * @throws IOException Could not read the stream. |
| 228 | */ |
| 229 | private JsonNode process( final InputStream in ) throws IOException { |
| 230 | final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in ); |
| 231 | setDocumentRoot( root ); |
| 232 | process( root ); |
| 233 | return getDocumentRoot(); |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Iterate over a given root node (at any level of the tree) and process each |
| 238 | * leaf node. |
| 239 | * |
| 240 | * @param root A node to process. |
| 241 | */ |
| 242 | private void process( final JsonNode root ) { |
| 243 | root.fields().forEachRemaining( this::process ); |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Process the given field, which is a named node. This is where the |
| 248 | * application does the up-front work of mapping references to their fully |
| 249 | * recursively dereferenced values. |
| 250 | * |
| 251 | * @param field The named node. |
| 252 | */ |
| 253 | private void process( final Entry<String, JsonNode> field ) { |
| 254 | final JsonNode node = field.getValue(); |
| 255 | |
| 256 | if( node.isObject() ) { |
| 257 | process( node ); |
| 258 | } |
| 259 | else { |
| 260 | final JsonNode fieldValue = field.getValue(); |
| 261 | |
| 262 | // Only basic data types can be parsed into variable values. For |
| 263 | // node structures, YAML has a built-in mechanism. |
| 264 | if( fieldValue.isValueNode() ) { |
| 265 | try { |
| 266 | resolve( fieldValue.asText() ); |
| 267 | } catch( StackOverflowError e ) { |
| 268 | setError( "Unresolvable: " + node.textValue() + " = " + fieldValue ); |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * Inserts the delimited references and field values into the cache. This will |
| 276 | * overwrite existing references. |
| 277 | * |
| 278 | * @param fieldValue YAML field containing zero or more delimited references. |
| 279 | * If it contains a delimited reference, the parameter is modified with the |
| 280 | * dereferenced value before it is returned. |
| 281 | * |
| 282 | * @return fieldValue without delimited references. |
| 283 | */ |
| 284 | private String resolve( String fieldValue ) { |
| 285 | final Matcher matcher = patternMatch( fieldValue ); |
| 286 | |
| 287 | while( matcher.find() ) { |
| 288 | final String delimited = matcher.group( GROUP_DELIMITED ); |
| 289 | final String reference = matcher.group( GROUP_REFERENCE ); |
| 290 | final String dereference = resolve( lookup( reference ) ); |
| 291 | |
| 292 | fieldValue = fieldValue.replace( delimited, dereference ); |
| 293 | |
| 294 | // This will perform some superfluous calls by overwriting existing |
| 295 | // items in the delimited reference map. |
| 296 | put( delimited, dereference ); |
| 297 | } |
| 298 | |
| 299 | return fieldValue; |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * Inserts a key/value pair into the references map. The map retains |
| 304 | * references and dereferenced values found in the YAML. If the reference |
| 305 | * already exists, this will overwrite with a new value. |
| 306 | * |
| 307 | * @param delimited The variable name. |
| 308 | * @param dereferenced The resolved value. |
| 309 | */ |
| 310 | private void put( String delimited, String dereferenced ) { |
| 311 | if( dereferenced.isEmpty() ) { |
| 312 | missing( delimited ); |
| 313 | } |
| 314 | else { |
| 315 | getReferences().put( delimited, dereferenced ); |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Writes the modified YAML document to standard output. |
| 321 | */ |
| 322 | private void writeDocument() throws IOException { |
| 323 | getObjectMapper().writeValue( System.out, getDocumentRoot() ); |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Called when a delimited reference is dereferenced to an empty string. This |
| 328 | * should produce a warning for the user. |
| 329 | * |
| 330 | * @param delimited Delimited reference with no derived value. |
| 331 | */ |
| 332 | private void missing( final String delimited ) { |
| 333 | setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) ); |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Returns a REGEX_PATTERN matcher for the given text. |
| 338 | * |
| 339 | * @param text The text that contains zero or more instances of a |
| 340 | * REGEX_PATTERN that can be found using the regular expression. |
| 341 | */ |
| 342 | private Matcher patternMatch( String text ) { |
| 343 | return getPattern().matcher( text ); |
| 344 | } |
| 345 | |
| 346 | /** |
| 347 | * Finds the YAML value for a reference. |
| 348 | * |
| 349 | * @param reference References a value in the YAML document. |
| 350 | * |
| 351 | * @return The dereferenced value. |
| 352 | */ |
| 353 | private String lookup( final String reference ) { |
| 354 | return getDocumentRoot().at( asPath( reference ) ).asText(); |
| 355 | } |
| 356 | |
| 357 | /** |
| 358 | * Converts a reference (not delimited) to a path that can be used to find a |
| 359 | * value that should exist inside the YAML document. |
| 360 | * |
| 361 | * @param reference The reference to convert to a YAML document path. |
| 362 | * |
| 363 | * @return The reference with a leading slash and its separator characters |
| 364 | * converted to slashes. |
| 365 | */ |
| 366 | private String asPath( final String reference ) { |
| 367 | return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML ); |
| 368 | } |
| 369 | |
| 370 | /** |
| 371 | * Sets the parent node for the entire YAML document tree. |
| 372 | * |
| 373 | * @param documentRoot The parent node. |
| 374 | */ |
| 375 | private void setDocumentRoot( final ObjectNode documentRoot ) { |
| 367 | 376 | this.documentRoot = documentRoot; |
| 368 | 377 | } |