001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.parser; 018 019import java.util.Map; 020 021import org.apache.wicket.util.lang.Objects; 022import org.apache.wicket.util.string.AppendingStringBuffer; 023import org.apache.wicket.util.string.StringValue; 024import org.apache.wicket.util.value.AttributeMap; 025import org.apache.wicket.util.value.IValueMap; 026 027 028/** 029 * A subclass of MarkupElement which represents a tag including namespace and its optional 030 * attributes. XmlTags are returned by the XML parser. 031 * 032 * @author Jonathan Locke 033 */ 034public class XmlTag 035{ 036 /** 037 * Enumerated type for different kinds of component tags. 038 */ 039 public static enum TagType { 040 /** A close tag, like </TAG>. */ 041 CLOSE, 042 043 /** An open tag, like <TAG componentId = "xyz">. */ 044 OPEN, 045 046 /** An open/close tag, like <TAG componentId = "xyz"/>. */ 047 OPEN_CLOSE; 048 } 049 050 TextSegment text; 051 052 /** Attribute map. */ 053 private AttributeMap attributes; 054 055 /** Name of tag, such as "img" or "input". */ 056 String name; 057 058 /** Namespace of the tag, if available, such as <wicket:link ...> */ 059 String namespace; 060 061 /** The tag type (OPEN, CLOSE or OPEN_CLOSE). */ 062 TagType type; 063 064 /** Any component tag that this tag closes. */ 065 private XmlTag closes; 066 067 /** If mutable, the immutable tag that this tag is a mutable copy of. */ 068 private XmlTag copyOf = this; 069 070 /** True if this tag is mutable, false otherwise. */ 071 private boolean isMutable = true; 072 073 /** 074 * Construct. 075 */ 076 public XmlTag() 077 { 078 super(); 079 } 080 081 /** 082 * Construct. 083 * 084 * @param text 085 * @param type 086 */ 087 public XmlTag(final TextSegment text, final TagType type) 088 { 089 this.text = text; 090 this.type = type; 091 } 092 093 /** 094 * Gets whether this tag closes the provided open tag. 095 * 096 * @param open 097 * The open tag 098 * @return True if this tag closes the given open tag 099 */ 100 public final boolean closes(final XmlTag open) 101 { 102 return (closes == open) || ((closes == open.copyOf) && (this != open)); 103 } 104 105 /** 106 * @param element 107 * @return true, if namespace, name and attributes are the same 108 */ 109 public final boolean equalTo(final XmlTag element) 110 { 111 final XmlTag that = element; 112 if (!Objects.equal(getNamespace(), that.getNamespace())) 113 { 114 return false; 115 } 116 if (!getName().equals(that.getName())) 117 { 118 return false; 119 } 120 return getAttributes().equals(that.getAttributes()); 121 } 122 123 /** 124 * Gets a hashmap of this tag's attributes. 125 * 126 * @return The tag's attributes 127 */ 128 public IValueMap getAttributes() 129 { 130 return attributes(); 131 } 132 133 private AttributeMap attributes() 134 { 135 if (attributes == null) 136 { 137 if ((copyOf == this) || (copyOf == null) || (copyOf.attributes == null)) 138 { 139 attributes = new AttributeMap(); 140 } 141 else 142 { 143 attributes = new AttributeMap(copyOf.attributes); 144 } 145 } 146 return attributes; 147 } 148 149 /** 150 * @return true if there 1 or more attributes. 151 */ 152 public boolean hasAttributes() 153 { 154 return attributes != null && attributes.size() > 0; 155 } 156 157 /** 158 * Get the column number. 159 * 160 * @return Returns the columnNumber. 161 */ 162 public int getColumnNumber() 163 { 164 return (text != null ? text.columnNumber : 0); 165 } 166 167 /** 168 * Gets the length of the tag in characters. 169 * 170 * @return The tag's length 171 */ 172 public int getLength() 173 { 174 return (text != null ? text.text.length() : 0); 175 } 176 177 /** 178 * Get the line number. 179 * 180 * @return Returns the lineNumber. 181 */ 182 public int getLineNumber() 183 { 184 return (text != null ? text.lineNumber : 0); 185 } 186 187 /** 188 * Gets the name of the tag, for example the tag <code><b></code>'s name would be 'b'. 189 * 190 * @return The tag's name 191 */ 192 public String getName() 193 { 194 return name; 195 } 196 197 /** 198 * Namespace of the tag, if available. For example, <wicket:link>. 199 * 200 * @return The tag's namespace 201 */ 202 public String getNamespace() 203 { 204 return namespace; 205 } 206 207 /** 208 * Assuming this is a close tag, return the corresponding open tag 209 * 210 * @return The open tag. Null, if no open tag available 211 */ 212 public final XmlTag getOpenTag() 213 { 214 return closes; 215 } 216 217 /** 218 * Gets the location of the tag in the input string. 219 * 220 * @return Tag location (index in input string) 221 */ 222 public int getPos() 223 { 224 return (text != null ? text.pos : 0); 225 } 226 227 /** 228 * Get a string attribute. 229 * 230 * @param key 231 * The key 232 * @return The string value 233 */ 234 public CharSequence getAttribute(final String key) 235 { 236 return getAttributes().getCharSequence(key); 237 } 238 239 /** 240 * Get the tag type. 241 * 242 * @return the tag type (OPEN, CLOSE or OPEN_CLOSE). 243 */ 244 public TagType getType() 245 { 246 return type; 247 } 248 249 /** 250 * Gets whether this is a close tag. 251 * 252 * @return True if this tag is a close tag 253 */ 254 public boolean isClose() 255 { 256 return type == TagType.CLOSE; 257 } 258 259 /** 260 * 261 * @return True, if tag is mutable 262 */ 263 public final boolean isMutable() 264 { 265 return isMutable; 266 } 267 268 /** 269 * Gets whether this is an open tag. 270 * 271 * @return True if this tag is an open tag 272 */ 273 public boolean isOpen() 274 { 275 return type == TagType.OPEN; 276 } 277 278 /** 279 * Gets whether this tag is an open/ close tag. 280 * 281 * @return True if this tag is an open and a close tag 282 */ 283 public boolean isOpenClose() 284 { 285 return type == TagType.OPEN_CLOSE; 286 } 287 288 /** 289 * Makes this tag object immutable by making the attribute map unmodifiable. Immutable tags 290 * cannot be made mutable again. They can only be copied into new mutable tag objects. 291 * 292 * @return this 293 */ 294 public XmlTag makeImmutable() 295 { 296 if (isMutable) 297 { 298 isMutable = false; 299 if (attributes != null) 300 { 301 attributes.makeImmutable(); 302 text = null; 303 } 304 } 305 return this; 306 } 307 308 /** 309 * Gets this tag if it is already mutable, or a mutable copy of this tag if it is immutable. 310 * 311 * @return This tag if it is already mutable, or a mutable copy of this tag if it is immutable. 312 */ 313 public XmlTag mutable() 314 { 315 if (isMutable) 316 { 317 return this; 318 } 319 else 320 { 321 final XmlTag tag = new XmlTag(); 322 copyPropertiesTo(tag); 323 return tag; 324 } 325 } 326 327 /** 328 * Copies all internal properties from this tag to <code>dest</code>. This is basically cloning 329 * without instance creation. 330 * 331 * @param dest 332 * tag whose properties will be set 333 */ 334 void copyPropertiesTo(final XmlTag dest) 335 { 336 dest.namespace = namespace; 337 dest.name = name; 338 dest.text = text; 339 dest.type = type; 340 dest.isMutable = true; 341 dest.closes = closes; 342 dest.copyOf = copyOf; 343 if (attributes != null) 344 { 345 dest.attributes = new AttributeMap(attributes); 346 } 347 } 348 349 /** 350 * Puts a boolean attribute. 351 * 352 * @param key 353 * The key 354 * @param value 355 * The value 356 * @return previous value associated with specified key, or null if there was no mapping for 357 * key. A null return can also indicate that the map previously associated null with the 358 * specified key, if the implementation supports null values. 359 */ 360 public Object put(final String key, final boolean value) 361 { 362 return put(key, Boolean.toString(value)); 363 } 364 365 /** 366 * Puts an int attribute. 367 * 368 * @param key 369 * The key 370 * @param value 371 * The value 372 * @return previous value associated with specified key, or null if there was no mapping for 373 * key. A null return can also indicate that the map previously associated null with the 374 * specified key, if the implementation supports null values. 375 */ 376 public Object put(final String key, final int value) 377 { 378 return put(key, Integer.toString(value)); 379 } 380 381 /** 382 * Puts a string attribute. 383 * 384 * @param key 385 * The key 386 * @param value 387 * The value 388 * @return previous value associated with specified key, or null if there was no mapping for 389 * key. A null return can also indicate that the map previously associated null with the 390 * specified key, if the implementation supports null values. 391 */ 392 public Object put(final String key, final CharSequence value) 393 { 394 return getAttributes().put(key, value); 395 } 396 397 /** 398 * Puts a {@link StringValue}attribute. 399 * 400 * @param key 401 * The key 402 * @param value 403 * The value 404 * @return previous value associated with specified key, or null if there was no mapping for 405 * key. A null return can also indicate that the map previously associated null with the 406 * specified key, if the implementation supports null values. 407 */ 408 public Object put(final String key, final StringValue value) 409 { 410 return getAttributes().put(key, (value != null) ? value.toString() : null); 411 } 412 413 /** 414 * Puts all attributes in map 415 * 416 * @param map 417 * A key/value map 418 */ 419 public void putAll(final Map<String, Object> map) 420 { 421 for (final Map.Entry<String, Object> entry : map.entrySet()) 422 { 423 Object value = entry.getValue(); 424 put(entry.getKey(), (value != null) ? value.toString() : null); 425 } 426 } 427 428 /** 429 * Removes an attribute. 430 * 431 * @param key 432 * The key to remove 433 */ 434 public void remove(final String key) 435 { 436 getAttributes().remove(key); 437 } 438 439 /** 440 * Sets the tag name. 441 * 442 * @param name 443 * New tag name 444 */ 445 public void setName(final String name) 446 { 447 if (isMutable) 448 { 449 this.name = name.intern(); 450 } 451 else 452 { 453 throw new UnsupportedOperationException("Attempt to set name of immutable tag"); 454 } 455 } 456 457 /** 458 * Sets the tag namespace. 459 * 460 * @param namespace 461 * New tag name 462 */ 463 public void setNamespace(final String namespace) 464 { 465 if (isMutable) 466 { 467 this.namespace = namespace != null ? namespace.intern() : null; 468 } 469 else 470 { 471 throw new UnsupportedOperationException("Attempt to set namespace of immutable tag"); 472 } 473 } 474 475 /** 476 * Assuming this is a close tag, assign it's corresponding open tag. 477 * 478 * @param tag 479 * the open-tag 480 * @throws RuntimeException 481 * if 'this' is not a close tag 482 */ 483 public void setOpenTag(final XmlTag tag) 484 { 485 closes = tag; 486 } 487 488 /** 489 * Sets type of this tag if it is not immutable. 490 * 491 * @param type 492 * The new type 493 */ 494 public void setType(final TagType type) 495 { 496 if (isMutable) 497 { 498 this.type = type; 499 } 500 else 501 { 502 throw new UnsupportedOperationException("Attempt to set type of immutable tag"); 503 } 504 } 505 506 /** 507 * Converts this object to a string representation. 508 * 509 * @return String version of this object 510 */ 511 public String toDebugString() 512 { 513 return "[Tag name = " + name + ", pos = " + text.pos + ", line = " + text.lineNumber + 514 ", attributes = [" + getAttributes() + "], type = " + type + "]"; 515 } 516 517 /** 518 * Converts this object to a string representation. 519 * 520 * @return String version of this object 521 */ 522 @Override 523 public String toString() 524 { 525 return toCharSequence().toString(); 526 } 527 528 /** 529 * @return The string representation of the tag 530 */ 531 public CharSequence toCharSequence() 532 { 533 if (!isMutable && (text != null)) 534 { 535 return text.text; 536 } 537 538 return toXmlString(); 539 } 540 541 /** 542 * String representation with line and column number 543 * 544 * @return String version of this object 545 */ 546 public String toUserDebugString() 547 { 548 return " '" + toString() + "' (line " + getLineNumber() + ", column " + getColumnNumber() + 549 ")"; 550 } 551 552 /** 553 * Assuming some attributes have been changed, toXmlString() rebuilds the String on based on the 554 * tags informations. 555 * 556 * @return A xml string matching the tag 557 */ 558 public CharSequence toXmlString() 559 { 560 final AppendingStringBuffer buffer = new AppendingStringBuffer(); 561 562 buffer.append('<'); 563 564 if (type == TagType.CLOSE) 565 { 566 buffer.append('/'); 567 } 568 569 if (namespace != null) 570 { 571 buffer.append(namespace); 572 buffer.append(':'); 573 } 574 575 buffer.append(name); 576 577 buffer.append(attributes().toCharSequence()); 578 579 if (type == TagType.OPEN_CLOSE) 580 { 581 buffer.append('/'); 582 } 583 584 buffer.append('>'); 585 return buffer; 586 } 587 588 static class TextSegment 589 { 590 /** Column number. */ 591 final int columnNumber; 592 593 /** Line number. */ 594 final int lineNumber; 595 596 /** Position of this tag in the input that was parsed. */ 597 final int pos; 598 599 /** Full text of tag. */ 600 final CharSequence text; 601 602 TextSegment(final CharSequence text, final int pos, final int line, final int col) 603 { 604 this.text = text; 605 this.pos = pos; 606 lineNumber = line; 607 columnNumber = col; 608 } 609 610 /** 611 * 612 * @return The xml markup text 613 */ 614 public final CharSequence getText() 615 { 616 return text; 617 } 618 619 /** 620 * @see java.lang.Object#toString() 621 */ 622 @Override 623 public String toString() 624 { 625 return text.toString(); 626 } 627 } 628}