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; 018 019import org.apache.wicket.Component; 020import org.apache.wicket.util.lang.Args; 021import org.apache.wicket.util.resource.IResourceStream; 022import org.apache.wicket.util.string.Strings; 023 024 025/** 026 * A stream of {@link org.apache.wicket.markup.MarkupElement}s, subclasses of which are 027 * {@link org.apache.wicket.markup.ComponentTag} and {@link org.apache.wicket.markup.RawMarkup}. A 028 * markup stream has a current index in the list of markup elements. The next markup element can be 029 * retrieved and the index advanced by calling next(). If the index hits the end, hasMore() will 030 * return false. 031 * <p> 032 * The current markup element can be accessed with get() and as a ComponentTag with getTag(). 033 * <p> 034 * The stream can be sought to a particular location with setCurrentIndex(). 035 * <p> 036 * Convenience methods also exist to skip component tags (and any potentially nested markup) or raw 037 * markup. 038 * <p> 039 * Several boolean methods of the form at*() return true if the markup stream is positioned at a tag 040 * with a given set of characteristics. 041 * <p> 042 * The resource from which the markup was loaded can be retrieved with getResource(). 043 * 044 * @author Jonathan Locke 045 */ 046public class MarkupStream 047{ 048 /** Element at currentIndex */ 049 private MarkupElement current; 050 051 /** Current index in markup stream */ 052 private int currentIndex = 0; 053 054 /** The markup element list */ 055 private final IMarkupFragment markup; 056 057 /** 058 * Constructor 059 * 060 * @param markup 061 * List of markup elements 062 */ 063 public MarkupStream(final IMarkupFragment markup) 064 { 065 Args.notNull(markup, "markup"); 066 067 this.markup = markup; 068 069 if (markup.size() > 0) 070 { 071 current = get(currentIndex); 072 } 073 } 074 075 /** 076 * @return True if current markup element is a close tag 077 */ 078 public boolean atCloseTag() 079 { 080 return atTag() && getTag().isClose(); 081 } 082 083 /** 084 * @return True if current markup element is an openclose tag 085 */ 086 public boolean atOpenCloseTag() 087 { 088 return atTag() && getTag().isOpenClose(); 089 } 090 091 /** 092 * @param componentId 093 * Required component name attribute 094 * @return True if the current markup element is an openclose tag with the given component name 095 */ 096 public boolean atOpenCloseTag(final String componentId) 097 { 098 return atOpenCloseTag() && componentId.equals(getTag().getId()); 099 } 100 101 /** 102 * @return True if current markup element is an open tag 103 */ 104 public boolean atOpenTag() 105 { 106 return atTag() && getTag().isOpen(); 107 } 108 109 /** 110 * @param id 111 * Required component id attribute 112 * @return True if the current markup element is an open tag with the given component name 113 */ 114 public boolean atOpenTag(final String id) 115 { 116 return atOpenTag() && id.equals(getTag().getId()); 117 } 118 119 /** 120 * @return True if current markup element is a tag 121 */ 122 public boolean atTag() 123 { 124 return current instanceof ComponentTag; 125 } 126 127 /** 128 * Compare this markup stream with another one 129 * 130 * @param that 131 * The other markup stream 132 * @return True if each MarkupElement in this matches each element in that 133 */ 134 public boolean equalTo(final MarkupStream that) 135 { 136 // While a has more markup elements 137 while (isCurrentIndexInsideTheStream()) 138 { 139 // Get an element from each 140 final MarkupElement thisElement = this.get(); 141 final MarkupElement thatElement = that.get(); 142 143 // and if the elements are not equal 144 if (thisElement != null && thatElement != null) 145 { 146 if (!thisElement.equalTo(thatElement)) 147 { 148 // fail the comparison 149 return false; 150 } 151 } 152 else 153 { 154 // If one element is null, 155 if (!(thisElement == null && thatElement == null)) 156 { 157 // fail the comparison 158 return false; 159 } 160 } 161 next(); 162 that.next(); 163 } 164 165 // If we've run out of markup elements in b 166 if (!that.isCurrentIndexInsideTheStream()) 167 { 168 // then the two streams match perfectly 169 return true; 170 } 171 172 // Stream b had extra elements 173 return false; 174 } 175 176 /** 177 * True, if associate markup is the same. It will change e.g. if the markup file has been 178 * re-loaded or the locale has been changed. 179 * 180 * @param markupStream 181 * The markup stream to compare with. 182 * @return true, if markup has not changed 183 */ 184 public final boolean equalMarkup(final MarkupStream markupStream) 185 { 186 if (markupStream == null) 187 { 188 return false; 189 } 190 return markup == markupStream.markup; 191 } 192 193 /** 194 * @return The current markup element 195 */ 196 public MarkupElement get() 197 { 198 return current; 199 } 200 201 /** 202 * @param index 203 * The index of a markup element 204 * @return The MarkupElement element 205 */ 206 public MarkupElement get(final int index) 207 { 208 return markup.get(index); 209 } 210 211 /** 212 * Get the component/container's Class which is directly associated with the stream. 213 * 214 * @return The component's class 215 */ 216 public final Class<? extends Component> getContainerClass() 217 { 218 return markup.getMarkupResourceStream().getMarkupClass(); 219 } 220 221 /** 222 * @return Current index in markup stream 223 */ 224 public int getCurrentIndex() 225 { 226 return currentIndex; 227 } 228 229 /** 230 * Gets the markup encoding. A markup encoding may be specified in a markup file with an XML 231 * encoding specifier of the form <?xml ... encoding="..." ?>. 232 * 233 * @return The encoding, or null if not found 234 */ 235 public final String getEncoding() 236 { 237 return markup.getMarkupResourceStream().getEncoding(); 238 } 239 240 /** 241 * @return The resource where this markup stream came from 242 */ 243 public IResourceStream getResource() 244 { 245 return markup.getMarkupResourceStream().getResource(); 246 } 247 248 /** 249 * @return The current markup element as a markup tag 250 */ 251 public ComponentTag getTag() 252 { 253 if (current instanceof ComponentTag) 254 { 255 return (ComponentTag)current; 256 } 257 258 throwMarkupException("Tag expected"); 259 260 return null; 261 } 262 263 /** 264 * Get the wicket namespace valid for this specific markup 265 * 266 * @return wicket namespace 267 */ 268 public final String getWicketNamespace() 269 { 270 return markup.getMarkupResourceStream().getWicketNamespace(); 271 } 272 273 /** 274 * @return True if this markup stream is moved to a MarkupElement element 275 */ 276 public boolean isCurrentIndexInsideTheStream() 277 { 278 return currentIndex < markup.size(); 279 } 280 281 /** 282 * @return True if this markup stream has more MarkupElement elements 283 */ 284 public boolean hasMore() 285 { 286 return currentIndex < (markup.size() - 1); 287 } 288 289 /** 290 * 291 * @return true, if underlying markup has been merged (inheritance) 292 */ 293 public final boolean isMergedMarkup() 294 { 295 return markup instanceof MergedMarkup; 296 } 297 298 /** 299 * Note: 300 * 301 * @return The next markup element in the stream 302 */ 303 public MarkupElement next() 304 { 305 if (++currentIndex < markup.size()) 306 { 307 return current = get(currentIndex); 308 } 309 310 return null; 311 } 312 313 /** 314 * Note: 315 * 316 * @return The next markup element in the stream 317 */ 318 public MarkupElement nextOpenTag() 319 { 320 while (next() != null) 321 { 322 MarkupElement elem = get(); 323 if (elem instanceof ComponentTag) 324 { 325 ComponentTag tag = (ComponentTag)elem; 326 if (tag.isOpen() || tag.isOpenClose()) 327 { 328 return current = get(currentIndex); 329 } 330 } 331 } 332 333 return null; 334 } 335 336 /** 337 * @param currentIndex 338 * New current index in the stream 339 * @return this 340 */ 341 public MarkupStream setCurrentIndex(final int currentIndex) 342 { 343 current = get(currentIndex); 344 this.currentIndex = currentIndex; 345 return this; 346 } 347 348 /** 349 * Skips this component and all nested components 350 */ 351 public final void skipComponent() 352 { 353 // Get start tag 354 final ComponentTag startTag = getTag(); 355 356 if (startTag.isOpen()) 357 { 358 // With HTML not all tags require a close tag which 359 // must have been detected by the HtmlHandler earlier on. 360 if (startTag.hasNoCloseTag() == false) 361 { 362 // Skip <tag> 363 next(); 364 365 // Skip nested components 366 skipToMatchingCloseTag(startTag); 367 } 368 369 // Skip </tag> 370 next(); 371 } 372 else if (startTag.isOpenClose()) 373 { 374 // Skip <tag/> 375 next(); 376 } 377 else 378 { 379 // We were something other than <tag> or <tag/> 380 throwMarkupException("Skip component called on bad markup element " + startTag); 381 } 382 } 383 384 /** 385 * Skips any raw markup at the current position 386 */ 387 public void skipRawMarkup() 388 { 389 while (true) 390 { 391 if (current instanceof RawMarkup) 392 { 393 if (next() != null) 394 { 395 continue; 396 } 397 } 398 else if ((current instanceof ComponentTag) && !(current instanceof WicketTag)) 399 { 400 ComponentTag tag = (ComponentTag)current; 401 if (tag.isAutoComponentTag()) 402 { 403 if (next() != null) 404 { 405 continue; 406 } 407 } 408 else if (tag.isClose() && tag.getOpenTag().isAutoComponentTag()) 409 { 410 if (next() != null) 411 { 412 continue; 413 } 414 } 415 } 416 break; 417 } 418 } 419 420 /** 421 * Skip until an element of type 'clazz' is found 422 * 423 * @param clazz 424 * @return true if found 425 */ 426 public boolean skipUntil(final Class<? extends MarkupElement> clazz) 427 { 428 while (isCurrentIndexInsideTheStream()) 429 { 430 if (clazz.isInstance(current)) 431 { 432 return true; 433 } 434 next(); 435 } 436 437 return false; 438 } 439 440 /** 441 * Skips any markup at the current position until the wicket tag name is found. 442 * 443 * @param wicketTagName 444 * wicket tag name to seek 445 */ 446 public void skipUntil(final String wicketTagName) 447 { 448 while (true) 449 { 450 if ((current instanceof WicketTag) && 451 ((WicketTag)current).getName().equals(wicketTagName)) 452 { 453 return; 454 } 455 456 // go on until we reach the end 457 if (next() == null) 458 { 459 return; 460 } 461 } 462 } 463 464 /** 465 * Renders markup until a closing tag for openTag is reached. 466 * 467 * @param openTag 468 * The open tag 469 */ 470 public void skipToMatchingCloseTag(final ComponentTag openTag) 471 { 472 // Loop through the markup in this container 473 while (isCurrentIndexInsideTheStream()) 474 { 475 // If the current markup tag closes the openTag 476 if (get().closes(openTag)) 477 { 478 // Done! 479 return; 480 } 481 482 // Skip element 483 next(); 484 } 485 throwMarkupException("Expected close tag for " + openTag); 486 } 487 488 /** 489 * @return A markup fragment starting at the current position 490 */ 491 public final IMarkupFragment getMarkupFragment() 492 { 493 return new MarkupFragment(markup, currentIndex); 494 } 495 496 /** 497 * Gets the attribute with 'name' for the tag at the current position 498 * 499 * @param name 500 * @param withWicketNamespace 501 * @return null, if not found 502 */ 503 public final String getTagAttribute(final String name, final boolean withWicketNamespace) 504 { 505 String attr = (withWicketNamespace ? attr = getWicketNamespace() + ":" + name : name); 506 return getTag().getAttributes().getString(attr); 507 } 508 509 /** 510 * Sometime its necessary to get the previous markup element versus the current one. 511 * 512 * @return The previous element (currentIndex - 1) 513 */ 514 public final ComponentTag getPreviousTag() 515 { 516 MarkupElement elem = get(currentIndex - 1); 517 if ((elem instanceof ComponentTag) == false) 518 { 519 throwMarkupException("Tag expected"); 520 } 521 522 return (ComponentTag)elem; 523 } 524 525 /** 526 * Throws a new markup exception 527 * 528 * @param message 529 * The exception message 530 * @throws MarkupException 531 */ 532 public void throwMarkupException(final String message) 533 { 534 throw new MarkupException(this, message); 535 } 536 537 /** 538 * @return An HTML string highlighting the current position in the markup stream 539 */ 540 public String toHtmlDebugString() 541 { 542 final StringBuilder buffer = new StringBuilder(); 543 544 for (int i = 0; i < markup.size(); i++) 545 { 546 if (i == currentIndex) 547 { 548 buffer.append("<font color = \"red\">"); 549 } 550 551 final MarkupElement element = markup.get(i); 552 553 buffer.append(Strings.escapeMarkup(element.toString(), true).toString()); 554 555 if (i == currentIndex) 556 { 557 buffer.append("</font>"); 558 } 559 } 560 561 return buffer.toString(); 562 } 563 564 /** 565 * @return String representation of markup stream 566 */ 567 @Override 568 public String toString() 569 { 570 return "[markup = " + String.valueOf(markup) + ", index = " + currentIndex + 571 ", current = " + ((current == null) ? "null" : current.toUserDebugString()) + "]"; 572 } 573}