| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
| HttpHeader |
|
| 2.066666666666667;2.067 | ||||
| HttpHeader$FieldIterator |
|
| 2.066666666666667;2.067 | ||||
| HttpHeader$TokenAndQValue |
|
| 2.066666666666667;2.067 | ||||
| HttpHeader$TokenAndQValueIterator |
|
| 2.066666666666667;2.067 | ||||
| HttpHeader$Tokenizer |
|
| 2.066666666666667;2.067 | ||||
| HttpHeader$WordIterator |
|
| 2.066666666666667;2.067 |
| 1 | /* | |
| 2 | * $Source$ | |
| 3 | * $Revision$ | |
| 4 | * | |
| 5 | * Copyright (C) 2003 Jim Wright | |
| 6 | * | |
| 7 | * Part of Melati (http://melati.org), a framework for the rapid | |
| 8 | * development of clean, maintainable web applications. | |
| 9 | * | |
| 10 | * Melati is free software; Permission is granted to copy, distribute | |
| 11 | * and/or modify this software under the terms either: | |
| 12 | * | |
| 13 | * a) the GNU General Public License as published by the Free Software | |
| 14 | * Foundation; either version 2 of the License, or (at your option) | |
| 15 | * any later version, | |
| 16 | * | |
| 17 | * or | |
| 18 | * | |
| 19 | * b) any version of the Melati Software License, as published | |
| 20 | * at http://melati.org | |
| 21 | * | |
| 22 | * You should have received a copy of the GNU General Public License and | |
| 23 | * the Melati Software License along with this program; | |
| 24 | * if not, write to the Free Software Foundation, Inc., | |
| 25 | * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the | |
| 26 | * GNU General Public License and visit http://melati.org to obtain the | |
| 27 | * Melati Software License. | |
| 28 | * | |
| 29 | * Feel free to contact the Developers of Melati (http://melati.org), | |
| 30 | * if you would like to work out a different arrangement than the options | |
| 31 | * outlined here. It is our intention to allow Melati to be used by as | |
| 32 | * wide an audience as possible. | |
| 33 | * | |
| 34 | * This program is distributed in the hope that it will be useful, | |
| 35 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 36 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 37 | * GNU General Public License for more details. | |
| 38 | * | |
| 39 | * Contact details for copyright holder: | |
| 40 | * | |
| 41 | * Jim Wright <jimw At paneris.org> | |
| 42 | * Bohemian Enterprise | |
| 43 | * Predmerice nad Jizerou 77 | |
| 44 | * 294 74 | |
| 45 | * Mlada Boleslav | |
| 46 | * Czech Republic | |
| 47 | */ | |
| 48 | ||
| 49 | package org.melati.util; | |
| 50 | ||
| 51 | import java.io.StreamTokenizer; | |
| 52 | import java.io.StringReader; | |
| 53 | import java.io.IOException; | |
| 54 | import java.util.Iterator; | |
| 55 | import java.util.Enumeration; | |
| 56 | ||
| 57 | /** | |
| 58 | * Representation of occurrences of an HTTP header field. | |
| 59 | * <p> | |
| 60 | * These are defined in RFC 2616 and have the same general form as in | |
| 61 | * RFC 822 section 3.1. | |
| 62 | * <P> | |
| 63 | * We generally assume that all continuation lines and occurrences in | |
| 64 | * a message are concatenated with comma separators. | |
| 65 | * | |
| 66 | * @author Jim Wright | |
| 67 | */ | |
| 68 | public class HttpHeader { | |
| 69 | ||
| 70 | /** | |
| 71 | * Instance of inner {@link Tokenizer}. | |
| 72 | */ | |
| 73 | protected Tokenizer tokenizer; | |
| 74 | ||
| 75 | /** | |
| 76 | * Create an instance representing the given comma separated fields. | |
| 77 | */ | |
| 78 | 912 | public HttpHeader(String values) { |
| 79 | 912 | if(values == null) |
| 80 | 566 | values= ""; |
| 81 | 912 | tokenizer = new Tokenizer(values); |
| 82 | 911 | } |
| 83 | ||
| 84 | /** | |
| 85 | * Abstract enumeration of fields. | |
| 86 | * <p> | |
| 87 | * Subtypes decide what type of token to return and how | |
| 88 | * to represent it. | |
| 89 | * <p> | |
| 90 | * This class serves to remove doubts about whether we should and can | |
| 91 | * implement <code>Iterator</code> or <code>Enumeration</code> and | |
| 92 | * proves itself unnecessary ;-). But we can factor stuff out and | |
| 93 | * re-use it later. | |
| 94 | * <p> | |
| 95 | * Actually, it also removes the need to think about exceptions in | |
| 96 | * subtypes. | |
| 97 | */ | |
| 98 | @SuppressWarnings("rawtypes") | |
| 99 | 911 | public abstract class FieldIterator implements Iterator<Object>, Enumeration{ |
| 100 | ||
| 101 | /** | |
| 102 | * {@inheritDoc} | |
| 103 | * @see java.util.Enumeration#hasMoreElements() | |
| 104 | */ | |
| 105 | public final boolean hasMoreElements() { | |
| 106 | 1314 | return hasNext(); |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * {@inheritDoc} | |
| 111 | * @see java.util.Enumeration#nextElement() | |
| 112 | */ | |
| 113 | public final Object nextElement() { | |
| 114 | 985 | return next(); |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * {@inheritDoc} | |
| 119 | * @see java.util.Iterator#hasNext() | |
| 120 | * @see #next() | |
| 121 | */ | |
| 122 | public final boolean hasNext() { | |
| 123 | 1907 | return tokenizer.ttype != StreamTokenizer.TT_EOF; |
| 124 | } | |
| 125 | ||
| 126 | /** | |
| 127 | * {@inheritDoc} | |
| 128 | * @see java.util.Iterator#remove() | |
| 129 | */ | |
| 130 | public void remove() throws UnsupportedOperationException { | |
| 131 | 1 | throw new UnsupportedOperationException("Cannot remove tokens from the HTTP header"); |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Return the next element or an exception. | |
| 136 | * | |
| 137 | * @return An exception if an object of the anticipated type cannot be returned | |
| 138 | */ | |
| 139 | public Object next() { | |
| 140 | try { | |
| 141 | 986 | return nextToken(); |
| 142 | } | |
| 143 | 1 | catch (HttpHeaderException e) { |
| 144 | 1 | return e; |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * @return the next token or throws an exception | |
| 150 | */ | |
| 151 | public abstract Object nextToken() throws HttpHeaderException; | |
| 152 | ||
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * Iteration over {@link HttpHeader.TokenAndQValue}s. | |
| 157 | */ | |
| 158 | 328 | public class WordIterator extends FieldIterator { |
| 159 | ||
| 160 | /** | |
| 161 | * @return the next word | |
| 162 | */ | |
| 163 | public String nextWord() throws HttpHeaderException { | |
| 164 | 984 | String result = tokenizer.readWord(); |
| 165 | 984 | tokenizer.skipAnyCommaSeparator(); |
| 166 | 984 | return result; |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * {@inheritDoc} | |
| 171 | * @see org.melati.util.HttpHeader.FieldIterator#nextToken() | |
| 172 | */ | |
| 173 | public Object nextToken() throws HttpHeaderException { | |
| 174 | 984 | return nextWord(); |
| 175 | } | |
| 176 | ||
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * Factory method to create and return an iterator of words. | |
| 181 | * | |
| 182 | * @return a new WordIterator | |
| 183 | */ | |
| 184 | public final WordIterator wordIterator() { | |
| 185 | 328 | return new WordIterator(); |
| 186 | } | |
| 187 | ||
| 188 | /** | |
| 189 | * Iteration over {@link HttpHeader.TokenAndQValue}s. | |
| 190 | */ | |
| 191 | 583 | public class TokenAndQValueIterator extends FieldIterator { |
| 192 | ||
| 193 | /** | |
| 194 | * @return the next TokenAndQValue | |
| 195 | * @throws HttpHeaderException | |
| 196 | */ | |
| 197 | public TokenAndQValue nextTokenAndQValue() throws HttpHeaderException { | |
| 198 | 12 | return HttpHeader.this.nextTokenAndQValue(); |
| 199 | } | |
| 200 | ||
| 201 | /** | |
| 202 | * {@inheritDoc} | |
| 203 | * @see org.melati.util.HttpHeader.FieldIterator#nextToken() | |
| 204 | */ | |
| 205 | public Object nextToken() throws HttpHeaderException { | |
| 206 | 2 | return nextTokenAndQValue(); |
| 207 | } | |
| 208 | ||
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Factory method to create and return the next | |
| 213 | * {@link HttpHeader.TokenAndQValue}. | |
| 214 | * @return a new TokenAndQValue | |
| 215 | */ | |
| 216 | public TokenAndQValue nextTokenAndQValue() throws HttpHeaderException { | |
| 217 | 12 | return new TokenAndQValue(tokenizer); |
| 218 | } | |
| 219 | ||
| 220 | /** | |
| 221 | * Factory method to create and return an iterator of {@link TokenAndQValue}'s. | |
| 222 | * @return a new TokenAndQValueIterator | |
| 223 | */ | |
| 224 | public TokenAndQValueIterator tokenAndQValueIterator() { | |
| 225 | 8 | return new TokenAndQValueIterator(); |
| 226 | } | |
| 227 | ||
| 228 | /** | |
| 229 | * A token and associated qvalue. | |
| 230 | */ | |
| 231 | public static class TokenAndQValue { | |
| 232 | ||
| 233 | /** | |
| 234 | * Token followed by a semicolon separator. | |
| 235 | */ | |
| 236 | public String token; | |
| 237 | ||
| 238 | /** | |
| 239 | * Value between zero and one with at most 3 decimal places. | |
| 240 | * <p> | |
| 241 | * q stands for "quality" but the RFC 2616 says this is not | |
| 242 | * completely accurate. | |
| 243 | * Values closer to 1.0 are better. | |
| 244 | * Zero means completely unfit. | |
| 245 | * <p> | |
| 246 | * The default is 1.0 if not explicitly initialised and this | |
| 247 | * appears to be correct for most possible uses if not all. | |
| 248 | */ | |
| 249 | 603 | public float q = 1.0f; |
| 250 | ||
| 251 | /** | |
| 252 | * Create an uninitialised instance. | |
| 253 | */ | |
| 254 | 603 | public TokenAndQValue() { |
| 255 | 603 | } |
| 256 | ||
| 257 | /** | |
| 258 | * Create an instance and initialise it by reading the given | |
| 259 | * tokenizer. | |
| 260 | */ | |
| 261 | public TokenAndQValue(Tokenizer t) throws HttpHeaderException { | |
| 262 | 26 | this(); |
| 263 | 26 | t.readTokenAndQValue(this); |
| 264 | 18 | t.skipAnyCommaSeparator(); |
| 265 | 18 | } |
| 266 | ||
| 267 | } | |
| 268 | ||
| 269 | /** | |
| 270 | * Tokenizer for parsing occurences of a field. | |
| 271 | * <p> | |
| 272 | * Header fields have format defined in RFC 2616 and have the same | |
| 273 | * general form as in RFC 822 section 3.1. | |
| 274 | * <p> | |
| 275 | * This is for fields consisting of tokens, quoted strings and | |
| 276 | * separators and not those consisting of an arbitrary sequence of | |
| 277 | * octets. | |
| 278 | * Tokens are US ASCII characters other than: | |
| 279 | * <ul> | |
| 280 | * <li> control characters 0000 to 001F and 007E; | |
| 281 | * <li> separators defined in RFC 2616; | |
| 282 | * </ul> | |
| 283 | * <p> | |
| 284 | * The convenience methods defined here provide some guidance on how | |
| 285 | * to interact with the super-type but you can also use inherited | |
| 286 | * methods. | |
| 287 | * <p> | |
| 288 | * We assume that the next token is always already read when a method | |
| 289 | * starts to interpret a sequence of tokens. | |
| 290 | * In other words the first token is read by the constructor(s) and then | |
| 291 | * each such | |
| 292 | * method returns as a result of reading a token or EOF that it cannot | |
| 293 | * process but without pushing it back. | |
| 294 | * The next token to be interpreted is hence the current token | |
| 295 | * described by the inherited instance variables. | |
| 296 | * <p> | |
| 297 | * Note that whitespace is automatically skipped by the supertype. | |
| 298 | * | |
| 299 | * @author Jim Wright | |
| 300 | */ | |
| 301 | public static class Tokenizer extends StreamTokenizer { | |
| 302 | ||
| 303 | /** | |
| 304 | * Create an instance from a string formed by concatenation of | |
| 305 | * continuation lines and all occurences of a field, with comma | |
| 306 | * separators. | |
| 307 | * <p> | |
| 308 | * In theory a separator can consist of one or more commas and | |
| 309 | * spaces and tab. | |
| 310 | * Fields are never empty. | |
| 311 | * We cope with this but I doubt typical callers ever encounter | |
| 312 | * such strings. | |
| 313 | * <p> | |
| 314 | * The field list should not be empty but null is | |
| 315 | * allowed to explicitly indicate that there are no such fields, | |
| 316 | * if an instance if required nevertheless to provide other | |
| 317 | * functionality. | |
| 318 | * | |
| 319 | * @param fields A non-null, non-empty String | |
| 320 | * @throws HttpHeaderException Error detected in the argument. | |
| 321 | */ | |
| 322 | Tokenizer(String fields) { | |
| 323 | 912 | super(new StringReader(fields)); |
| 324 | ||
| 325 | ||
| 326 | 912 | resetSyntax(); |
| 327 | // Initially make all non-control characters token | |
| 328 | // characters | |
| 329 | 912 | wordChars('\u0020', '\u007E'); |
| 330 | // Now change separators back. Tab is not | |
| 331 | // necessary and there are some ranges but let's | |
| 332 | // not try and be clever. | |
| 333 | 912 | String separator = "()<>@,;:\\\"/[]?={} \t"; |
| 334 | 18240 | for (int i = 0; i < separator.length(); i++) { |
| 335 | 17328 | ordinaryChar(separator.charAt(i)); |
| 336 | // System.err.println("Tested 34"); | |
| 337 | } | |
| 338 | ||
| 339 | // Resetting effectively did this to whitespace chars | |
| 340 | // ordinaryChars('\u0000', '\u0020'); | |
| 341 | // Set space and table characters as whitespace | |
| 342 | 912 | whitespaceChars(' ', ' '); |
| 343 | 912 | whitespaceChars('\t', '\t'); |
| 344 | ||
| 345 | 912 | quoteChar('"'); |
| 346 | ||
| 347 | 912 | parseNumbers(); |
| 348 | ||
| 349 | // Here are some things we have effectively done by resetting | |
| 350 | // ordinaryChar('/'); | |
| 351 | // ordinaryChar('\''); | |
| 352 | ||
| 353 | // Do not do any other special processing | |
| 354 | 912 | eolIsSignificant(false); |
| 355 | 912 | lowerCaseMode(false); |
| 356 | 912 | slashSlashComments(false); |
| 357 | 912 | slashStarComments(false); |
| 358 | ||
| 359 | // Read the first token | |
| 360 | 912 | nextLToken(); |
| 361 | 912 | if (ttype == ',') { |
| 362 | // System.err.println("Tested 36"); | |
| 363 | 1 | throw new HttpHeaderException("HTTP header fields starts with comma separator"); |
| 364 | } | |
| 365 | 911 | } |
| 366 | ||
| 367 | /** | |
| 368 | * Same as <code>nextToken()</code> but does not throw an <code>IOException</code> | |
| 369 | * and handles erroneous line breaks. | |
| 370 | * | |
| 371 | * @return int value of next LToken | |
| 372 | * @throws HttpHeaderException Error detected in the fields. | |
| 373 | */ | |
| 374 | public int nextLToken() throws HttpHeaderException { | |
| 375 | int result; | |
| 376 | try { | |
| 377 | 2623 | result = nextToken(); |
| 378 | 2623 | if (ttype == TT_EOL) { |
| 379 | 0 | System.err.println("Not tested 38"); |
| 380 | 0 | throw new HttpHeaderException("HTTP header fields span unquoted line breaks"); |
| 381 | } | |
| 382 | // System.err.println("Tested 39"); | |
| 383 | 2623 | return result; |
| 384 | } | |
| 385 | 0 | catch (IOException e) { |
| 386 | //assert false : "We are reading from a string"; | |
| 387 | 0 | return 0; |
| 388 | } | |
| 389 | } | |
| 390 | ||
| 391 | /** | |
| 392 | * Read up to and including the next token after comma | |
| 393 | * separator(s) and whitespace assuming the current token is a comma. | |
| 394 | * | |
| 395 | * @return Resulting ttype. | |
| 396 | */ | |
| 397 | public final int skipCommaSeparator() throws HttpHeaderException { | |
| 398 | 665 | if (ttype != ',') { |
| 399 | 0 | throw new IllegalStateException("Not at a comma"); |
| 400 | } | |
| 401 | 667 | while (nextLToken() == ',') |
| 402 | 2 | ; |
| 403 | 665 | return ttype; |
| 404 | } | |
| 405 | ||
| 406 | /** | |
| 407 | * Read up to and including the next token after any comma | |
| 408 | * separator(s) and whitespace. | |
| 409 | * <p> | |
| 410 | * This is the same as {@link #skipCommaSeparator()} but it does | |
| 411 | * nothing if we are and EOF. | |
| 412 | * | |
| 413 | * @return Resulting ttype. | |
| 414 | */ | |
| 415 | public final int skipAnyCommaSeparator() throws HttpHeaderException { | |
| 416 | 1002 | if (ttype != TT_EOF) { |
| 417 | 665 | skipCommaSeparator(); |
| 418 | } | |
| 419 | 1002 | return ttype; |
| 420 | } | |
| 421 | ||
| 422 | /** | |
| 423 | * Convenience method to test for token or quoted string. | |
| 424 | * <p> | |
| 425 | * If this returns true then the token value is in <code>sval</code> | |
| 426 | * with any quotes removed. | |
| 427 | * @return whether token is an SVal | |
| 428 | */ | |
| 429 | public final boolean isSVal() { | |
| 430 | 26 | return ttype == TT_WORD || ttype == '"'; |
| 431 | } | |
| 432 | ||
| 433 | /** | |
| 434 | * Read the word token or quoted string that comes next. | |
| 435 | * | |
| 436 | * @return the SVal | |
| 437 | * @throws HttpHeaderException Error detected in the fields. | |
| 438 | */ | |
| 439 | public final String readSVal() throws HttpHeaderException { | |
| 440 | 26 | if (! isSVal()) { |
| 441 | 2 | throw new HttpHeaderException("Next token is not a (possibly quoted) word: " + |
| 442 | 2 | toString()); |
| 443 | } | |
| 444 | 24 | String result = sval; |
| 445 | 24 | nextLToken(); |
| 446 | 24 | return result; |
| 447 | } | |
| 448 | ||
| 449 | /** | |
| 450 | * Read the word token that comes next. | |
| 451 | * | |
| 452 | * @return the word as a String | |
| 453 | * @throws HttpHeaderException Error detected in the fields. | |
| 454 | */ | |
| 455 | public final String readWord() throws HttpHeaderException { | |
| 456 | 995 | if (ttype != TT_WORD) { |
| 457 | 1 | throw new HttpHeaderException("Next token is not a word token: " + |
| 458 | 1 | toString()); |
| 459 | } | |
| 460 | 994 | String result = sval; |
| 461 | 994 | nextLToken(); |
| 462 | // System.err.println("Tested 47"); | |
| 463 | 994 | return result; |
| 464 | } | |
| 465 | ||
| 466 | /** | |
| 467 | * Read the given word token that comes next. | |
| 468 | * | |
| 469 | * @throws HttpHeaderException Error detected in the fields. | |
| 470 | */ | |
| 471 | public final void readWord(String word) throws HttpHeaderException { | |
| 472 | 11 | String read = readWord(); |
| 473 | 10 | if (! read.equals(word)) { |
| 474 | // System.err.println("Tested 48 by temporary hack"); | |
| 475 | 1 | throw new HttpHeaderException("Expecting '" + word + |
| 476 | 1 | "' but encountered: " + toString()); |
| 477 | } | |
| 478 | 9 | } |
| 479 | ||
| 480 | /** | |
| 481 | * Read the given character that comes next. | |
| 482 | * | |
| 483 | * @throws HttpHeaderException Error detected in the fields. | |
| 484 | */ | |
| 485 | public final void readChar(char c) throws HttpHeaderException { | |
| 486 | 20 | if (ttype != c) { |
| 487 | // System.err.println("Tested 49"); | |
| 488 | 1 | throw new HttpHeaderException("Expecting '" + c + |
| 489 | "' but encountered: " + | |
| 490 | 1 | toString()); |
| 491 | } | |
| 492 | 19 | nextLToken(); |
| 493 | 19 | } |
| 494 | ||
| 495 | /** | |
| 496 | * Read the number token that comes next. | |
| 497 | * @return the number's value as a double | |
| 498 | * @throws HttpHeaderException Error detected in the fields. | |
| 499 | */ | |
| 500 | public final double readNVal() throws HttpHeaderException { | |
| 501 | 8 | if (ttype != TT_NUMBER) { |
| 502 | 1 | throw new HttpHeaderException("Next token is not a number: " + |
| 503 | 1 | toString()); |
| 504 | } | |
| 505 | 7 | double result = nval; |
| 506 | 7 | nextLToken(); |
| 507 | 7 | return result; |
| 508 | } | |
| 509 | ||
| 510 | /** | |
| 511 | * Read a token sequence of the form "; q = 0.42" and return the number. | |
| 512 | * @return the number's value as a float | |
| 513 | * | |
| 514 | * @throws IllegalStateException Current token not semicolon. | |
| 515 | * @throws HttpHeaderException Error detected in the fields. | |
| 516 | */ | |
| 517 | public final float readQValue() | |
| 518 | throws IllegalStateException, HttpHeaderException { | |
| 519 | 11 | if (ttype != ';') { |
| 520 | 0 | throw new IllegalStateException("Not at a semicolon"); |
| 521 | } | |
| 522 | 11 | readChar(';'); |
| 523 | 11 | readWord("q"); |
| 524 | 9 | readChar('='); |
| 525 | 8 | return (float)readNVal(); |
| 526 | } | |
| 527 | ||
| 528 | /** | |
| 529 | * Read a word or quoted string token optionally followed by a string | |
| 530 | * of the form "; q = 0.42" and initialises the given object. | |
| 531 | * @return current TokenAndQValue | |
| 532 | */ | |
| 533 | protected TokenAndQValue readTokenAndQValue(TokenAndQValue result) | |
| 534 | throws HttpHeaderException { | |
| 535 | 26 | result.token = readSVal(); |
| 536 | 24 | switch (ttype) { |
| 537 | case TT_EOF : | |
| 538 | case ',' : | |
| 539 | 11 | break; |
| 540 | case ';' : | |
| 541 | 11 | result.q = readQValue(); |
| 542 | 7 | break; |
| 543 | default: | |
| 544 | 2 | throw new HttpHeaderException("Word token: \'" + result.token + |
| 545 | 2 | "\' is followed by something unexpected: " + toString()); |
| 546 | } | |
| 547 | 18 | return result; |
| 548 | } | |
| 549 | ||
| 550 | } | |
| 551 | ||
| 552 | ||
| 553 | public static class HttpHeaderException extends MelatiRuntimeException { | |
| 554 | ||
| 555 | private static final long serialVersionUID = -8870151118057435290L; | |
| 556 | ||
| 557 | /** | |
| 558 | * Create an instance with message. | |
| 559 | */ | |
| 560 | public HttpHeaderException(String message) { | |
| 561 | super(message); | |
| 562 | } | |
| 563 | ||
| 564 | /** | |
| 565 | * Create an instance with message and cause. | |
| 566 | */ | |
| 567 | public HttpHeaderException(String message, Exception e) { | |
| 568 | super(message, e); | |
| 569 | } | |
| 570 | ||
| 571 | } | |
| 572 | ||
| 573 | } |