001 /* 002 * JBoss DNA (http://www.jboss.org/dna) 003 * See the COPYRIGHT.txt file distributed with this work for information 004 * regarding copyright ownership. Some portions may be licensed 005 * to Red Hat, Inc. under one or more contributor license agreements. 006 * See the AUTHORS.txt file in the distribution for a full listing of 007 * individual contributors. 008 * 009 * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA 010 * is licensed to you under the terms of the GNU Lesser General Public License as 011 * published by the Free Software Foundation; either version 2.1 of 012 * the License, or (at your option) any later version. 013 * 014 * JBoss DNA is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 017 * Lesser General Public License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this software; if not, write to the Free 021 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 022 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 023 */ 024 package org.jboss.dna.graph.property; 025 026 import java.io.Serializable; 027 import java.util.Iterator; 028 import java.util.List; 029 import net.jcip.annotations.Immutable; 030 import org.jboss.dna.common.text.Jsr283Encoder; 031 import org.jboss.dna.common.text.NoOpEncoder; 032 import org.jboss.dna.common.text.TextDecoder; 033 import org.jboss.dna.common.text.TextEncoder; 034 import org.jboss.dna.common.text.UrlEncoder; 035 import org.jboss.dna.graph.property.basic.BasicName; 036 import org.jboss.dna.graph.property.basic.BasicPathSegment; 037 038 /** 039 * An object representation of a node path within a repository. 040 * <p> 041 * A path consists of zero or more segments that can contain any characters, although the string representation may require some 042 * characters to be encoded. For example, if a path contains a segment with a forward slash, then this forward slash must be 043 * escaped when writing the whole path to a string (since a forward slash is used as the {@link #DELIMITER delimiter} between 044 * segments). 045 * </p> 046 * <p> 047 * Because of this encoding and decoding issue, there is no standard representation of a path as a string. Instead, this class 048 * uses {@link TextEncoder text encoders} to escape certain characters when writing to a string or unescaping the string 049 * representation. These encoders and used only with individual segments, and therefore are not used to encode the 050 * {@link #DELIMITER delimiter}. Three standard encoders are provided, although others can certainly be used: 051 * <ul> 052 * <li>{@link #JSR283_ENCODER Jsr283Encoder} - an encoder and decoder that is compliant with <a 053 * href="http://jcp.org/en/jsr/detail?id=283">JSR-283</a> by converting the reserved characters (namely '*', '/', ':', '[', ']' 054 * and '|') to their unicode equivalent.</td> 055 * </li> 056 * <li>{@link #URL_ENCODER UrlEncoder} - an encoder and decoder that is useful for converting text to be used within a URL, as 057 * defined by Section 2.3 of <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>. This encoder does encode many characters 058 * (including '`', '@', '#', '$', '^', '&', '{', '[', '}', ']', '|', ':', ';', '\', '"', '<', ',', '>', '?', '/', and ' '), while 059 * others are not encoded (including '-', '_', '.', '!', '~', '*', '\', ''', '(', and ')'). Note that only the '*' character is 060 * the only character reserved by JSR-283 that is not encoded by the URL encoder.</li> 061 * <li>{@link #NO_OP_ENCODER NoOpEncoder} - an {@link TextEncoder encoder} implementation that does nothing.</li> 062 * </ul> 063 * </p> 064 * <p> 065 * This class simplifies working with paths and using a <code>Path</code> is often more efficient that processing and 066 * manipulating the equivalent <code>String</code>. This class can easily {@link #iterator() iterate} over the segments, return 067 * the {@link #size() number of segments}, {@link #compareTo(Path) compare} with other paths, {@link #resolve(Path) resolve} 068 * relative paths, return the {@link #getParent() ancestor (or parent)}, determine whether one path is an 069 * {@link #isAncestorOf(Path) ancestor} or {@link #isDecendantOf(Path) decendent} of another path, and 070 * {@link #getCommonAncestor(Path) finding a common ancestor}. 071 * </p> 072 * 073 * @author Randall Hauch 074 * @author John Verhaeg 075 */ 076 @Immutable 077 public interface Path extends Comparable<Path>, Iterable<Path.Segment>, Serializable { 078 079 /** 080 * The text encoder that does nothing. 081 */ 082 public static final TextEncoder NO_OP_ENCODER = new NoOpEncoder(); 083 084 /** 085 * The text encoder that encodes according to JSR-283. 086 */ 087 public static final TextEncoder JSR283_ENCODER = new Jsr283Encoder(); 088 089 /** 090 * The text encoder that encodes text according to the rules of <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>. 091 */ 092 public static final TextEncoder URL_ENCODER = new UrlEncoder().setSlashEncoded(true); 093 094 /** 095 * The text decoder that does nothing. 096 */ 097 public static final TextDecoder NO_OP_DECODER = new NoOpEncoder(); 098 099 /** 100 * The text decoder that decodes according to JSR-283. 101 */ 102 public static final TextDecoder JSR283_DECODER = new Jsr283Encoder(); 103 104 /** 105 * The text decoder that decodes text according to the rules of <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>. 106 */ 107 public static final TextDecoder URL_DECODER = new UrlEncoder().setSlashEncoded(true); 108 109 /** 110 * The default text encoder to be used when none is otherwise specified. This is currently the {@link #JSR283_ENCODER JSR-283 111 * encoder}. 112 */ 113 public static final TextEncoder DEFAULT_ENCODER = JSR283_ENCODER; 114 115 /** 116 * The default text decoder to be used when none is otherwise specified. This is currently the {@link #JSR283_ENCODER JSR-283 117 * encoder}. 118 */ 119 public static final TextDecoder DEFAULT_DECODER = JSR283_DECODER; 120 121 /** 122 * The delimiter character used to separate segments within a path. 123 */ 124 public static final char DELIMITER = '/'; 125 126 /** 127 * String form of the delimiter used to separate segments within a path. 128 */ 129 public static final String DELIMITER_STR = new String(new char[] {DELIMITER}); 130 131 /** 132 * String representation of the segment that references a parent. 133 */ 134 public static final String PARENT = ".."; 135 136 /** 137 * String representation of the segment that references the same segment. 138 */ 139 public static final String SELF = "."; 140 141 /** 142 * The default index for a {@link Segment}. 143 */ 144 public static final int DEFAULT_INDEX = 1; 145 146 /** 147 * Representation of the segments that occur within a path. 148 * 149 * @author Randall Hauch 150 */ 151 @Immutable 152 public static interface Segment extends Cloneable, Comparable<Segment>, Serializable { 153 154 /** 155 * Get the name component of this segment. 156 * 157 * @return the segment's name 158 */ 159 public Name getName(); 160 161 /** 162 * Get the index for this segment, which will be 1 by default. 163 * 164 * @return the index 165 */ 166 public int getIndex(); 167 168 /** 169 * Return whether this segment has an index. 170 * 171 * @return true if this segment has an index, or false otherwise. 172 */ 173 public boolean hasIndex(); 174 175 /** 176 * Return whether this segment is a self-reference. 177 * 178 * @return true if the segment is a self-reference, or false otherwise. 179 */ 180 public boolean isSelfReference(); 181 182 /** 183 * Return whether this segment is a reference to a parent. 184 * 185 * @return true if the segment is a parent-reference, or false otherwise. 186 */ 187 public boolean isParentReference(); 188 189 /** 190 * Get the raw string form of the segment using the {@link Path#NO_OP_ENCODER no-op encoder}. This is equivalent to 191 * calling <code>getString(Path.NO_OP_ENCODER)</code>. 192 * 193 * @return the un-encoded string 194 * @see #getString(TextEncoder) 195 */ 196 public String getUnencodedString(); 197 198 /** 199 * Get the string form of the segment. The {@link #DEFAULT_ENCODER default encoder} is used to encode characters in each 200 * of the path segments. 201 * 202 * @return the encoded string 203 * @see #getString(TextEncoder) 204 */ 205 public String getString(); 206 207 /** 208 * Get the encoded string form of the segment, using the supplied encoder to encode characters in each of the path 209 * segments. 210 * 211 * @param encoder the encoder to use, or null if the {@link #DEFAULT_ENCODER default encoder} should be used 212 * @return the encoded string 213 * @see #getString() 214 */ 215 public String getString( TextEncoder encoder ); 216 217 /** 218 * Get the string form of the segment, using the supplied namespace registry to convert the name's namespace URI to a 219 * prefix. The {@link #DEFAULT_ENCODER default encoder} is used to encode characters in each of the path segments. 220 * 221 * @param namespaceRegistry the namespace registry that should be used to obtain the prefix for the 222 * {@link Name#getNamespaceUri() namespace URI} in the segment's {@link #getName() name} 223 * @return the encoded string 224 * @throws IllegalArgumentException if the namespace registry is null 225 * @see #getString(NamespaceRegistry,TextEncoder) 226 */ 227 public String getString( NamespaceRegistry namespaceRegistry ); 228 229 /** 230 * Get the encoded string form of the segment, using the supplied namespace registry to convert the name's namespace URI 231 * to a prefix and the supplied encoder to encode characters in each of the path segments. 232 * 233 * @param namespaceRegistry the namespace registry that should be used to obtain the prefix for the 234 * {@link Name#getNamespaceUri() namespace URI} in the segment's {@link #getName() name} 235 * @param encoder the encoder to use, or null if the {@link #DEFAULT_ENCODER default encoder} should be used 236 * @return the encoded string 237 * @throws IllegalArgumentException if the namespace registry is null 238 * @see #getString(NamespaceRegistry) 239 */ 240 public String getString( NamespaceRegistry namespaceRegistry, 241 TextEncoder encoder ); 242 243 /** 244 * Get the encoded string form of the segment, using the supplied namespace registry to convert the names' namespace URIs 245 * to prefixes and the supplied encoder to encode characters in each of the path segments. The second encoder is used to 246 * encode (or convert) the delimiter between the {@link Name#getNamespaceUri() namespace prefix} and the 247 * {@link Name#getLocalName() local part}. 248 * 249 * @param namespaceRegistry the namespace registry that should be used to obtain the prefix for the 250 * {@link Name#getNamespaceUri() namespace URIs} in the segment {@link Segment#getName() names} 251 * @param encoder the encoder to use for encoding the {@link Name#getLocalName() local part} and 252 * {@link Name#getNamespaceUri() namespace prefix} in the segment's {@link #getName() name}, or null if the 253 * {@link #DEFAULT_ENCODER default encoder} should be used 254 * @param delimiterEncoder the encoder to use for encoding the delimiter between the {@link Name#getLocalName() local 255 * part} and {@link Name#getNamespaceUri() namespace prefix} of each {@link Path#getSegmentsList() segment}, or 256 * null if the standard delimiters should be used 257 * @return the encoded string 258 * @see #getString(NamespaceRegistry) 259 * @see #getString(NamespaceRegistry, TextEncoder) 260 */ 261 public String getString( NamespaceRegistry namespaceRegistry, 262 TextEncoder encoder, 263 TextEncoder delimiterEncoder ); 264 } 265 266 /** 267 * Singleton instance of the name referencing a self, provided as a convenience. 268 */ 269 public static final Name SELF_NAME = new BasicName(null, SELF); 270 271 /** 272 * Singleton instance of the name referencing a parent, provided as a convenience. 273 */ 274 public static final Name PARENT_NAME = new BasicName(null, PARENT); 275 276 /** 277 * Singleton instance of the path segment referencing a parent, provided as a convenience. 278 */ 279 public static final Path.Segment SELF_SEGMENT = new BasicPathSegment(SELF_NAME); 280 281 /** 282 * Singleton instance of the path segment referencing a parent, provided as a convenience. 283 */ 284 public static final Path.Segment PARENT_SEGMENT = new BasicPathSegment(PARENT_NAME); 285 286 /** 287 * Return the number of segments in this path. 288 * 289 * @return the number of path segments 290 */ 291 public int size(); 292 293 /** 294 * Return whether this path represents the root path. 295 * 296 * @return true if this path is the root path, or false otherwise 297 */ 298 public boolean isRoot(); 299 300 /** 301 * Determine whether this path represents the same as the supplied path. This is equivalent to calling <code> 302 * this.compareTo(other) == 0 </code>. 303 * 304 * @param other the other path to compare with this path; may be null 305 * @return true if the paths are equivalent, or false otherwise 306 */ 307 public boolean isSameAs( Path other ); 308 309 /** 310 * Determine whether this path is the {@link #isSameAs(Path) same as} to or a {@link #isAncestorOf(Path) ancestor of} the 311 * supplied path. This method is equivalent to (but may be more efficient than) calling <code>isSame(other) || 312 * isAncestor(other)</code>, and is a convenience method that is identical to calling <code>other.isAtOrBelow(this)</code>. 313 * 314 * @param other the other path to compare with this path; may be null 315 * @return true if the paths are equivalent or if this path is considered an ancestor of the other path, or false otherwise 316 */ 317 public boolean isAtOrAbove( Path other ); 318 319 /** 320 * Determine whether this path is the {@link #isSameAs(Path) same as} to or a {@link #isDecendantOf(Path) decendant of} the 321 * supplied path. This method is equivalent to (but may be more efficient than) calling <code>isSame(other) || 322 * isAncestor(other)</code>. 323 * 324 * @param other the other path to compare with this path; may be null 325 * @return true if the paths are equivalent or if this path is considered a decendant of the other path, or false otherwise 326 */ 327 public boolean isAtOrBelow( Path other ); 328 329 /** 330 * Determine whether this path is an ancestor of the supplied path. A path is considered an ancestor of another path if the 331 * the ancestor path appears in its entirety at the beginning of the decendant path, and where the decendant path contains at 332 * least one additional segment. 333 * 334 * @param decendant the path that may be the decendant; may be null 335 * @return true if this path is an ancestor of the supplied path, or false otherwise 336 */ 337 public boolean isAncestorOf( Path decendant ); 338 339 /** 340 * Determine whether this path is an decendant of the supplied path. A path is considered a decendant of another path if the 341 * the decendant path starts exactly with the entire ancestor path but contains at least one additional segment. 342 * 343 * @param ancestor the path that may be the ancestor; may be null 344 * @return true if this path is an decendant of the supplied path, or false otherwise 345 */ 346 public boolean isDecendantOf( Path ancestor ); 347 348 /** 349 * Return whether this path is an absolute path. A path is either relative or {@link #isAbsolute() absolute}. An absolute path 350 * starts with a "/". 351 * 352 * @return true if the path is absolute, or false otherwise 353 */ 354 public boolean isAbsolute(); 355 356 /** 357 * Return whether this path is normalized and contains no "." segments and as few ".." segments as possible. For example, the 358 * path "../a" is normalized, while "/a/b/c/../d" is not normalized. 359 * 360 * @return true if this path is normalized, or false otherwise 361 */ 362 public boolean isNormalized(); 363 364 /** 365 * Get a normalized path with as many ".." segments and all "." resolved. The relative path ".", however, will return itself 366 * as the normalized path, since it cannot be resolved any further. 367 * 368 * @return the normalized path, or this object if this path is already normalized 369 * @throws InvalidPathException if the normalized form would result in a path with negative length (e.g., "/a/../../..") 370 */ 371 public Path getNormalizedPath(); 372 373 /** 374 * Get the canonical form of this path. A canonical path has is {@link #isAbsolute() absolute} and {@link #isNormalized()}. 375 * 376 * @return the canonical path, or this object if it is already in its canonical form 377 * @throws InvalidPathException if the path is not absolute and cannot be canonicalized 378 */ 379 public Path getCanonicalPath(); 380 381 /** 382 * Get a relative path from the supplied path to this path. 383 * 384 * @param startingPath the path specifying the starting point for the new relative path; may not be null 385 * @return the relative path 386 * @throws IllegalArgumentException if the supplied path is null 387 * @throws PathNotFoundException if both this path and the supplied path are not absolute 388 */ 389 public Path relativeTo( Path startingPath ); 390 391 /** 392 * Get the absolute path by resolving the supplied relative (non-absolute) path against this absolute path. 393 * 394 * @param relativePath the relative path that is to be resolved against this path 395 * @return the absolute and normalized path resolved from this path and the supplied absolute path 396 * @throws IllegalArgumentException if the supplied path is null 397 * @throws InvalidPathException if the this path is not absolute or if the supplied path is not relative. 398 */ 399 public Path resolve( Path relativePath ); 400 401 /** 402 * Get the absolute path by resolving this relative (non-absolute) path against the supplied absolute path. 403 * 404 * @param absolutePath the absolute path to which this relative path should be resolve 405 * @return the absolute path resolved from this path and the supplied absolute path 406 * @throws IllegalArgumentException if the supplied path is null 407 * @throws InvalidPathException if the supplied path is not absolute or if this path is not relative. 408 */ 409 public Path resolveAgainst( Path absolutePath ); 410 411 /** 412 * Return the path to the parent, or this path if it is the {@link #isRoot() root}. This is an efficient operation that does 413 * not require copying any data. 414 * 415 * @return the parent path, or this null if it is already the root 416 */ 417 public Path getParent(); 418 419 /** 420 * Return the path to the ancestor of the supplied degree. An ancestor of degree <code>x</code> is the path that is <code>x 421 * </code> levels up along the path. For example, <code>degree = 0</code> returns this path, while <code>degree = 1</code> 422 * returns the parent of this path, <code>degree = 2</code> returns the grandparent of this path, and so on. Note that the 423 * result may be unexpected if this path is not {@link #isNormalized() normalized}, as a non-normalized path contains ".." and 424 * "." segments. 425 * 426 * @param degree 427 * @return the ancestor of the supplied degree 428 * @throws IllegalArgumentException if the degree is negative 429 * @throws InvalidPathException if the degree is greater than the {@link #size() length} of this path 430 */ 431 public Path getAncestor( int degree ); 432 433 /** 434 * Determine whether this path and the supplied path have the same immediate ancestor. In other words, this method determines 435 * whether the node represented by this path is a sibling of the node represented by the supplied path. 436 * 437 * @param that the other path 438 * @return true if this path and the supplied path have the same immediate ancestor. 439 * @throws IllegalArgumentException if the supplied path is null 440 */ 441 public boolean hasSameAncestor( Path that ); 442 443 /** 444 * Find the lowest common ancestor of this path and the supplied path. 445 * 446 * @param that the other path 447 * @return the lowest common ancestor, which may be the root path if there is no other. 448 * @throws IllegalArgumentException if the supplied path is null 449 */ 450 public Path getCommonAncestor( Path that ); 451 452 /** 453 * Get the last segment in this path. 454 * 455 * @return the last segment, or null if the path is empty 456 */ 457 public Segment getLastSegment(); 458 459 /** 460 * Get the segment at the supplied index. 461 * 462 * @param index the index 463 * @return the segment 464 * @throws IndexOutOfBoundsException if the index is out of bounds 465 */ 466 public Segment getSegment( int index ); 467 468 /** 469 * Return a new path consisting of the segments starting at <code>beginIndex</code> index (inclusive). This is equivalent to 470 * calling <code>path.subpath(beginIndex,path.size()-1)</code>. 471 * 472 * @param beginIndex the beginning index, inclusive. 473 * @return the specified subpath 474 * @exception IndexOutOfBoundsException if the <code>beginIndex</code> is negative or larger than the length of this <code> 475 * Path</code> object 476 */ 477 public Path subpath( int beginIndex ); 478 479 /** 480 * Return a new path consisting of the segments between the <code>beginIndex</code> index (inclusive) and the <code>endIndex 481 * </code> index (exclusive). 482 * 483 * @param beginIndex the beginning index, inclusive. 484 * @param endIndex the ending index, exclusive. 485 * @return the specified subpath 486 * @exception IndexOutOfBoundsException if the <code>beginIndex</code> is negative, or <code>endIndex</code> is larger than 487 * the length of this <code>Path</code> object, or <code>beginIndex</code> is larger than <code>endIndex</code>. 488 */ 489 public Path subpath( int beginIndex, 490 int endIndex ); 491 492 /** 493 * {@inheritDoc} 494 */ 495 public Iterator<Segment> iterator(); 496 497 /** 498 * Return an iterator that walks the paths from the root path down to this path. This method always returns at least one path 499 * (the root returns an iterator containing itself). 500 * 501 * @return the path iterator; never null 502 */ 503 public Iterator<Path> pathsFromRoot(); 504 505 /** 506 * Obtain a copy of the segments in this path. None of the segments are encoded. 507 * 508 * @return the array of segments as a copy 509 */ 510 public Segment[] getSegmentsArray(); 511 512 /** 513 * Get an unmodifiable list of the path segments. 514 * 515 * @return the unmodifiable list of path segments; never null 516 */ 517 public List<Segment> getSegmentsList(); 518 519 /** 520 * Get the string form of the path. The {@link #DEFAULT_ENCODER default encoder} is used to encode characters in each of the 521 * path segments. 522 * 523 * @return the encoded string 524 * @see #getString(TextEncoder) 525 */ 526 public String getString(); 527 528 /** 529 * Get the encoded string form of the path, using the supplied encoder to encode characters in each of the path segments. 530 * 531 * @param encoder the encoder to use, or null if the {@link #DEFAULT_ENCODER default encoder} should be used 532 * @return the encoded string 533 * @see #getString() 534 */ 535 public String getString( TextEncoder encoder ); 536 537 /** 538 * Get the string form of the path, using the supplied namespace registry to convert the names' namespace URIs to prefixes. 539 * The {@link #DEFAULT_ENCODER default encoder} is used to encode characters in each of the path segments. The second encoder 540 * is used to encode (or convert) the delimiter between the {@link Name#getNamespaceUri() namespace prefix} and the 541 * {@link Name#getLocalName() local part}. 542 * 543 * @param namespaceRegistry the namespace registry that should be used to obtain the prefix for the 544 * {@link Name#getNamespaceUri() namespace URIs} in the segment {@link Segment#getName() names} 545 * @return the encoded string 546 * @throws IllegalArgumentException if the namespace registry is null 547 * @see #getString(NamespaceRegistry,TextEncoder) 548 * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder) 549 */ 550 public String getString( NamespaceRegistry namespaceRegistry ); 551 552 /** 553 * Get the encoded string form of the path, using the supplied namespace registry to convert the names' namespace URIs to 554 * prefixes and the supplied encoder to encode characters in each of the path segments. 555 * 556 * @param namespaceRegistry the namespace registry that should be used to obtain the prefix for the 557 * {@link Name#getNamespaceUri() namespace URIs} in the segment {@link Segment#getName() names}, or null if the 558 * namespace registry should not be used 559 * @param encoder the encoder to use for encoding the {@link Name#getLocalName() local part} and 560 * {@link Name#getNamespaceUri() namespace prefix} of each {@link Path#getSegmentsList() segment}, or null if the 561 * {@link #DEFAULT_ENCODER default encoder} should be used 562 * @return the encoded string 563 * @see #getString(NamespaceRegistry) 564 * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder) 565 */ 566 public String getString( NamespaceRegistry namespaceRegistry, 567 TextEncoder encoder ); 568 569 /** 570 * Get the encoded string form of the path, using the supplied namespace registry to convert the names' namespace URIs to 571 * prefixes and the supplied encoder to encode characters in each of the path segments. 572 * 573 * @param namespaceRegistry the namespace registry that should be used to obtain the prefix for the 574 * {@link Name#getNamespaceUri() namespace URIs} in the segment {@link Segment#getName() names} 575 * @param encoder the encoder to use for encoding the {@link Name#getLocalName() local part} and 576 * {@link Name#getNamespaceUri() namespace prefix} of each {@link Path#getSegmentsList() segment}, or null if the 577 * {@link #DEFAULT_ENCODER default encoder} should be used 578 * @param delimiterEncoder the encoder to use for encoding the delimiter between the {@link Name#getLocalName() local part} 579 * and {@link Name#getNamespaceUri() namespace prefix} of each {@link Path#getSegmentsList() segment}, and for encoding 580 * the path delimiter, or null if the standard delimiters should be used 581 * @return the encoded string 582 * @see #getString(NamespaceRegistry) 583 * @see #getString(NamespaceRegistry, TextEncoder) 584 */ 585 public String getString( NamespaceRegistry namespaceRegistry, 586 TextEncoder encoder, 587 TextEncoder delimiterEncoder ); 588 589 }