001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.html; 014 015import static org.apache.juneau.internal.StringUtils.*; 016import static org.apache.juneau.internal.ObjectUtils.*; 017import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*; 018 019import java.io.*; 020import java.util.*; 021import java.util.regex.*; 022 023import org.apache.juneau.*; 024import org.apache.juneau.html.annotation.*; 025import org.apache.juneau.internal.*; 026import org.apache.juneau.serializer.*; 027import org.apache.juneau.transform.*; 028import org.apache.juneau.xml.*; 029import org.apache.juneau.xml.annotation.*; 030 031/** 032 * Session object that lives for the duration of a single use of {@link HtmlSerializer}. 033 * 034 * <p> 035 * This class is NOT thread safe. 036 * It is typically discarded after one-time use although it can be reused within the same thread. 037 */ 038public class HtmlSerializerSession extends XmlSerializerSession { 039 040 private final HtmlSerializer ctx; 041 private final Pattern urlPattern = Pattern.compile("http[s]?\\:\\/\\/.*"); 042 private final Pattern labelPattern; 043 044 045 /** 046 * Create a new session using properties specified in the context. 047 * 048 * @param ctx 049 * The context creating this session object. 050 * The context contains all the configuration settings for this object. 051 * @param args 052 * Runtime arguments. 053 * These specify session-level information such as locale and URI context. 054 * It also include session-level properties that override the properties defined on the bean and 055 * serializer contexts. 056 */ 057 protected HtmlSerializerSession(HtmlSerializer ctx, SerializerSessionArgs args) { 058 super(ctx, args); 059 this.ctx = ctx; 060 labelPattern = Pattern.compile("[\\?\\&]" + Pattern.quote(ctx.getLabelParameter()) + "=([^\\&]*)"); 061 } 062 063 @Override /* Session */ 064 public ObjectMap asMap() { 065 return super.asMap() 066 .append("HtmlSerializerSession", new ObjectMap() 067 ); 068 } 069 070 /** 071 * Converts the specified output target object to an {@link HtmlWriter}. 072 * 073 * @param out The output target object. 074 * @return The output target object wrapped in an {@link HtmlWriter}. 075 * @throws Exception 076 */ 077 protected final HtmlWriter getHtmlWriter(SerializerPipe out) throws Exception { 078 Object output = out.getRawOutput(); 079 if (output instanceof HtmlWriter) 080 return (HtmlWriter)output; 081 HtmlWriter w = new HtmlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), 082 getUriResolver()); 083 out.setWriter(w); 084 return w; 085 } 086 087 /** 088 * Returns <jk>true</jk> if the specified object is a URL. 089 * 090 * @param cm The ClassMeta of the object being serialized. 091 * @param pMeta 092 * The property metadata of the bean property of the object. 093 * Can be <jk>null</jk> if the object isn't from a bean property. 094 * @param o The object. 095 * @return <jk>true</jk> if the specified object is a URL. 096 */ 097 public boolean isUri(ClassMeta<?> cm, BeanPropertyMeta pMeta, Object o) { 098 if (cm.isUri()) 099 return true; 100 if (pMeta != null && pMeta.isUri()) 101 return true; 102 if (isDetectLinksInStrings() && o instanceof CharSequence && urlPattern.matcher(o.toString()).matches()) 103 return true; 104 return false; 105 } 106 107 /** 108 * Returns the anchor text to use for the specified URL object. 109 * 110 * @param pMeta 111 * The property metadata of the bean property of the object. 112 * Can be <jk>null</jk> if the object isn't from a bean property. 113 * @param o The URL object. 114 * @return The anchor text to use for the specified URL object. 115 */ 116 public String getAnchorText(BeanPropertyMeta pMeta, Object o) { 117 String s = o.toString(); 118 if (isLookForLabelParameters()) { 119 Matcher m = labelPattern.matcher(s); 120 if (m.find()) 121 return urlDecode(m.group(1)); 122 } 123 switch (getUriAnchorText()) { 124 case LAST_TOKEN: 125 s = resolveUri(s); 126 if (s.indexOf('/') != -1) 127 s = s.substring(s.lastIndexOf('/')+1); 128 if (s.indexOf('?') != -1) 129 s = s.substring(0, s.indexOf('?')); 130 if (s.indexOf('#') != -1) 131 s = s.substring(0, s.indexOf('#')); 132 if (s.isEmpty()) 133 s = "/"; 134 return urlDecode(s); 135 case URI_ANCHOR: 136 if (s.indexOf('#') != -1) 137 s = s.substring(s.lastIndexOf('#')+1); 138 return urlDecode(s); 139 case PROPERTY_NAME: 140 return pMeta == null ? s : pMeta.getName(); 141 case URI: 142 return resolveUri(s); 143 case CONTEXT_RELATIVE: 144 return relativizeUri("context:/", s); 145 case SERVLET_RELATIVE: 146 return relativizeUri("servlet:/", s); 147 case PATH_RELATIVE: 148 return relativizeUri("request:/", s); 149 default /* TO_STRING */: 150 return s; 151 } 152 } 153 154 @Override /* XmlSerializer */ 155 public boolean isHtmlMode() { 156 return true; 157 } 158 159 @Override /* Serializer */ 160 protected void doSerialize(SerializerPipe out, Object o) throws Exception { 161 doSerialize(o, getHtmlWriter(out)); 162 } 163 164 /** 165 * Main serialization routine. 166 * 167 * @param session The serialization context object. 168 * @param o The object being serialized. 169 * @param w The writer to serialize to. 170 * @return The same writer passed in. 171 * @throws IOException If a problem occurred trying to send output to the writer. 172 */ 173 private XmlWriter doSerialize(Object o, XmlWriter w) throws Exception { 174 serializeAnything(w, o, getExpectedRootType(o), null, null, getInitialDepth()-1, true); 175 return w; 176 } 177 178 @SuppressWarnings({ "rawtypes", "unchecked" }) 179 @Override /* XmlSerializerSession */ 180 protected ContentResult serializeAnything( 181 XmlWriter out, 182 Object o, 183 ClassMeta<?> eType, 184 String elementName, 185 Namespace elementNamespace, 186 boolean addNamespaceUris, 187 XmlFormat format, 188 boolean isMixed, 189 boolean preserveWhitespace, 190 BeanPropertyMeta pMeta) throws Exception { 191 192 // If this is a bean, then we want to serialize it as HTML unless it's @Html(format=XML). 193 ClassMeta<?> type = push(elementName, o, eType); 194 pop(); 195 196 if (type == null) 197 type = object(); 198 else if (type.isDelegate()) 199 type = ((Delegate)o).getClassMeta(); 200 PojoSwap swap = type.getPojoSwap(this); 201 if (swap != null) { 202 o = swap.swap(this, o); 203 type = swap.getSwapClassMeta(this); 204 if (type.isObject()) 205 type = getClassMetaForObject(o); 206 } 207 208 HtmlClassMeta cHtml = cHtml(type); 209 210 if (type.isMapOrBean() && ! cHtml.isXml()) 211 return serializeAnything(out, o, eType, elementName, pMeta, 0, false); 212 213 return super.serializeAnything(out, o, eType, elementName, elementNamespace, addNamespaceUris, format, isMixed, preserveWhitespace, pMeta); 214 } 215 /** 216 * Serialize the specified object to the specified writer. 217 * 218 * @param out The writer. 219 * @param o The object to serialize. 220 * @param eType The expected type of the object if this is a bean property. 221 * @param name 222 * The attribute name of this object if this object was a field in a JSON object (i.e. key of a 223 * {@link java.util.Map.Entry} or property name of a bean). 224 * @param pMeta The bean property being serialized, or <jk>null</jk> if we're not serializing a bean property. 225 * @param xIndent The current indentation value. 226 * @param isRoot <jk>true</jk> if this is the root element of the document. 227 * @return The type of content encountered. Either simple (no whitespace) or normal (elements with whitespace). 228 * @throws Exception If a problem occurred trying to convert the output. 229 */ 230 @SuppressWarnings({ "rawtypes", "unchecked" }) 231 protected ContentResult serializeAnything(XmlWriter out, Object o, 232 ClassMeta<?> eType, String name, BeanPropertyMeta pMeta, int xIndent, boolean isRoot) throws Exception { 233 234 ClassMeta<?> aType = null; // The actual type 235 ClassMeta<?> wType = null; // The wrapped type (delegate) 236 ClassMeta<?> sType = object(); // The serialized type 237 238 if (eType == null) 239 eType = object(); 240 241 aType = push(name, o, eType); 242 243 // Handle recursion 244 if (aType == null) { 245 o = null; 246 aType = object(); 247 } 248 249 indent += xIndent; 250 251 ContentResult cr = CR_ELEMENTS; 252 253 // Determine the type. 254 if (o == null || (aType.isChar() && ((Character)o).charValue() == 0)) { 255 out.tag("null"); 256 cr = ContentResult.CR_MIXED; 257 258 } else { 259 260 if (aType.isDelegate()) { 261 wType = aType; 262 aType = ((Delegate)o).getClassMeta(); 263 } 264 265 sType = aType; 266 267 String typeName = null; 268 if (isAddBeanTypes() && ! eType.equals(aType)) 269 typeName = aType.getDictionaryName(); 270 271 // Swap if necessary 272 PojoSwap swap = aType.getPojoSwap(this); 273 if (swap != null) { 274 o = swap.swap(this, o); 275 sType = swap.getSwapClassMeta(this); 276 277 // If the getSwapClass() method returns Object, we need to figure out 278 // the actual type now. 279 if (sType.isObject()) 280 sType = getClassMetaForObject(o); 281 } 282 283 // Handle the case where we're serializing a raw stream. 284 if (sType.isReader() || sType.isInputStream()) { 285 pop(); 286 indent -= xIndent; 287 IOUtils.pipe(o, out); 288 return ContentResult.CR_MIXED; 289 } 290 291 HtmlClassMeta cHtml = cHtml(sType); 292 HtmlBeanPropertyMeta bpHtml = bpHtml(pMeta); 293 294 HtmlRender render = firstNonNull(bpHtml.getRender(), cHtml.getRender()); 295 296 if (render != null) { 297 Object o2 = render.getContent(this, o); 298 if (o2 != o) { 299 indent -= xIndent; 300 pop(); 301 out.nl(indent); 302 return serializeAnything(out, o2, null, typeName, null, xIndent, false); 303 } 304 } 305 306 if (cHtml.isXml() || bpHtml.isXml()) { 307 pop(); 308 indent++; 309 super.serializeAnything(out, o, null, null, null, false, XmlFormat.MIXED, false, false, null); 310 indent -= xIndent+1; 311 return cr; 312 313 } else if (cHtml.isPlainText() || bpHtml.isPlainText()) { 314 out.write(o == null ? "null" : o.toString()); 315 cr = CR_MIXED; 316 317 } else if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) { 318 out.tag("null"); 319 cr = CR_MIXED; 320 321 } else if (sType.isNumber()) { 322 if (eType.isNumber() && ! isRoot) 323 out.append(o); 324 else 325 out.sTag("number").append(o).eTag("number"); 326 cr = CR_MIXED; 327 328 } else if (sType.isBoolean()) { 329 if (eType.isBoolean() && ! isRoot) 330 out.append(o); 331 else 332 out.sTag("boolean").append(o).eTag("boolean"); 333 cr = CR_MIXED; 334 335 } else if (sType.isMap() || (wType != null && wType.isMap())) { 336 out.nlIf(! isRoot, xIndent+1); 337 if (o instanceof BeanMap) 338 serializeBeanMap(out, (BeanMap)o, eType, pMeta); 339 else 340 serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), typeName, pMeta); 341 342 } else if (sType.isBean()) { 343 BeanMap m = toBeanMap(o); 344 Class<?> c = o.getClass(); 345 if (c.isAnnotationPresent(HtmlLink.class)) { 346 HtmlLink h = o.getClass().getAnnotation(HtmlLink.class); 347 Object urlProp = m.get(h.uriProperty()); 348 Object nameProp = m.get(h.nameProperty()); 349 out.oTag("a").attrUri("href", urlProp).append('>').text(nameProp).eTag("a"); 350 cr = CR_MIXED; 351 } else { 352 out.nlIf(! isRoot, xIndent+2); 353 serializeBeanMap(out, m, eType, pMeta); 354 } 355 356 } else if (sType.isCollection() || sType.isArray() || (wType != null && wType.isCollection())) { 357 out.nlIf(! isRoot, xIndent+1); 358 serializeCollection(out, o, sType, eType, name, pMeta); 359 360 } else if (isUri(sType, pMeta, o)) { 361 String label = getAnchorText(pMeta, o); 362 out.oTag("a").attrUri("href", o).append('>'); 363 out.text(label); 364 out.eTag("a"); 365 cr = CR_MIXED; 366 367 } else { 368 if (isRoot) 369 out.sTag("string").text(toString(o)).eTag("string"); 370 else 371 out.text(toString(o)); 372 cr = CR_MIXED; 373 } 374 } 375 pop(); 376 indent -= xIndent; 377 return cr; 378 } 379 380 @SuppressWarnings({ "rawtypes" }) 381 private void serializeMap(XmlWriter out, Map m, ClassMeta<?> sType, 382 ClassMeta<?> eKeyType, ClassMeta<?> eValueType, String typeName, BeanPropertyMeta ppMeta) throws Exception { 383 384 ClassMeta<?> keyType = eKeyType == null ? string() : eKeyType; 385 ClassMeta<?> valueType = eValueType == null ? object() : eValueType; 386 ClassMeta<?> aType = getClassMetaForObject(m); // The actual type 387 HtmlClassMeta cHtml = cHtml(aType); 388 HtmlBeanPropertyMeta bpHtml = bpHtml(ppMeta); 389 390 int i = indent; 391 392 out.oTag(i, "table"); 393 394 if (typeName != null && ppMeta != null && ppMeta.getClassMeta() != aType) 395 out.attr(getBeanTypePropertyName(sType), typeName); 396 397 out.append(">").nl(i+1); 398 if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) { 399 out.sTag(i+1, "tr").nl(i+2); 400 out.sTag(i+2, "th").append("key").eTag("th").nl(i+3); 401 out.sTag(i+2, "th").append("value").eTag("th").nl(i+3); 402 out.ie(i+1).eTag("tr").nl(i+2); 403 } 404 for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) { 405 406 Object key = generalize(e.getKey(), keyType); 407 Object value = null; 408 try { 409 value = e.getValue(); 410 } catch (StackOverflowError t) { 411 throw t; 412 } catch (Throwable t) { 413 onError(t, "Could not call getValue() on property ''{0}'', {1}", e.getKey(), t.getLocalizedMessage()); 414 } 415 416 String link = getLink(ppMeta); 417 String style = getStyle(this, ppMeta, value); 418 419 out.sTag(i+1, "tr").nl(i+2); 420 out.oTag(i+2, "td"); 421 if (style != null) 422 out.attr("style", style); 423 out.cTag(); 424 if (link != null) 425 out.oTag(i+3, "a").attrUri("href", link.replace("{#}", asString(value))).cTag(); 426 ContentResult cr = serializeAnything(out, key, keyType, null, null, 2, false); 427 if (link != null) 428 out.eTag("a"); 429 if (cr == CR_ELEMENTS) 430 out.i(i+2); 431 out.eTag("td").nl(i+2); 432 out.sTag(i+2, "td"); 433 cr = serializeAnything(out, value, valueType, (key == null ? "_x0000_" : toString(key)), null, 2, false); 434 if (cr == CR_ELEMENTS) 435 out.ie(i+2); 436 out.eTag("td").nl(i+2); 437 out.ie(i+1).eTag("tr").nl(i+1); 438 } 439 out.ie(i).eTag("table").nl(i); 440 } 441 442 private void serializeBeanMap(XmlWriter out, BeanMap<?> m, ClassMeta<?> eType, BeanPropertyMeta ppMeta) throws Exception { 443 444 HtmlClassMeta cHtml = cHtml(m.getClassMeta()); 445 HtmlBeanPropertyMeta bpHtml = bpHtml(ppMeta); 446 447 int i = indent; 448 449 out.oTag(i, "table"); 450 451 String typeName = m.getMeta().getDictionaryName(); 452 if (typeName != null && eType != m.getClassMeta()) 453 out.attr(getBeanTypePropertyName(m.getClassMeta()), typeName); 454 455 out.append('>').nl(i); 456 if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) { 457 out.sTag(i+1, "tr").nl(i+1); 458 out.sTag(i+2, "th").append("key").eTag("th").nl(i+2); 459 out.sTag(i+2, "th").append("value").eTag("th").nl(i+2); 460 out.ie(i+1).eTag("tr").nl(i+1); 461 } 462 463 for (BeanPropertyValue p : m.getValues(isTrimNullProperties())) { 464 BeanPropertyMeta pMeta = p.getMeta(); 465 ClassMeta<?> cMeta = p.getClassMeta(); 466 467 String key = p.getName(); 468 Object value = p.getValue(); 469 Throwable t = p.getThrown(); 470 if (t != null) 471 onBeanGetterException(pMeta, t); 472 473 if (canIgnoreValue(cMeta, key, value)) 474 continue; 475 476 String link = null, anchorText = null; 477 if (! cMeta.isCollectionOrArray()) { 478 link = m.resolveVars(getLink(pMeta)); 479 anchorText = m.resolveVars(getAnchorText(pMeta)); 480 } 481 482 if (anchorText != null) 483 value = anchorText; 484 485 out.sTag(i+1, "tr").nl(i+1); 486 out.sTag(i+2, "td").text(key).eTag("td").nl(i+2); 487 out.oTag(i+2, "td"); 488 String style = getStyle(this, pMeta, value); 489 if (style != null) 490 out.attr("style", style); 491 out.cTag(); 492 493 try { 494 if (link != null) 495 out.oTag(i+3, "a").attrUri("href", link).cTag(); 496 ContentResult cr = serializeAnything(out, value, cMeta, key, pMeta, 2, false); 497 if (cr == CR_ELEMENTS) 498 out.i(i+2); 499 if (link != null) 500 out.eTag("a"); 501 } catch (SerializeException e) { 502 throw e; 503 } catch (Error e) { 504 throw e; 505 } catch (Throwable e) { 506 e.printStackTrace(); 507 onBeanGetterException(pMeta, e); 508 } 509 out.eTag("td").nl(i+2); 510 out.ie(i+1).eTag("tr").nl(i+1); 511 } 512 out.ie(i).eTag("table").nl(i); 513 } 514 515 @SuppressWarnings({ "rawtypes", "unchecked" }) 516 private void serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, String name, BeanPropertyMeta ppMeta) throws Exception { 517 518 HtmlClassMeta cHtml = cHtml(sType); 519 HtmlBeanPropertyMeta bpHtml = bpHtml(ppMeta); 520 521 Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in)); 522 523 boolean isCdc = cHtml.isHtmlCdc() || bpHtml.isHtmlCdc(); 524 boolean isSdc = cHtml.isHtmlSdc() || bpHtml.isHtmlSdc(); 525 boolean isDc = isCdc || isSdc; 526 527 int i = indent; 528 if (c.isEmpty()) { 529 out.appendln(i, "<ul></ul>"); 530 return; 531 } 532 533 String type2 = null; 534 if (sType != eType) 535 type2 = sType.getDictionaryName(); 536 if (type2 == null) 537 type2 = "array"; 538 539 c = sort(c); 540 541 String btpn = getBeanTypePropertyName(eType); 542 543 // Look at the objects to see how we're going to handle them. Check the first object to see how we're going to 544 // handle this. 545 // If it's a map or bean, then we'll create a table. 546 // Otherwise, we'll create a list. 547 Object[] th = getTableHeaders(c, bpHtml); 548 549 if (th != null) { 550 551 out.oTag(i, "table").attr(btpn, type2).append('>').nl(i+1); 552 out.sTag(i+1, "tr").nl(i+2); 553 for (Object key : th) { 554 out.sTag(i+2, "th"); 555 out.text(convertToType(key, String.class)); 556 out.eTag("th").nl(i+2); 557 } 558 out.ie(i+1).eTag("tr").nl(i+1); 559 560 for (Object o : c) { 561 ClassMeta<?> cm = getClassMetaForObject(o); 562 563 if (cm != null && cm.getPojoSwap(this) != null) { 564 PojoSwap swap = cm.getPojoSwap(this); 565 o = swap.swap(this, o); 566 cm = swap.getSwapClassMeta(this); 567 } 568 569 out.oTag(i+1, "tr"); 570 String typeName = (cm == null ? null : cm.getDictionaryName()); 571 String typeProperty = getBeanTypePropertyName(cm); 572 573 if (typeName != null && eType.getElementType() != cm) 574 out.attr(typeProperty, typeName); 575 out.cTag().nl(i+2); 576 577 if (cm == null) { 578 out.i(i+2); 579 serializeAnything(out, o, null, null, null, 1, false); 580 out.nl(0); 581 582 } else if (cm.isMap() && ! (cm.isBeanMap())) { 583 Map m2 = sort((Map)o); 584 585 for (Object k : th) { 586 out.sTag(i+2, "td"); 587 ContentResult cr = serializeAnything(out, m2.get(k), eType.getElementType(), toString(k), null, 2, false); 588 if (cr == CR_ELEMENTS) 589 out.i(i+2); 590 out.eTag("td").nl(i+2); 591 } 592 } else { 593 BeanMap m2 = null; 594 if (o instanceof BeanMap) 595 m2 = (BeanMap)o; 596 else 597 m2 = toBeanMap(o); 598 599 for (Object k : th) { 600 BeanMapEntry p = m2.getProperty(toString(k)); 601 BeanPropertyMeta pMeta = p.getMeta(); 602 if (pMeta.canRead()) { 603 Object value = p.getValue(); 604 605 String link = null, anchorText = null; 606 if (! pMeta.getClassMeta().isCollectionOrArray()) { 607 link = m2.resolveVars(getLink(pMeta)); 608 anchorText = m2.resolveVars(getAnchorText(pMeta)); 609 } 610 611 if (anchorText != null) 612 value = anchorText; 613 614 String style = getStyle(this, pMeta, value); 615 out.oTag(i+2, "td"); 616 if (style != null) 617 out.attr("style", style); 618 out.cTag(); 619 if (link != null) 620 out.oTag("a").attrUri("href", link).cTag(); 621 ContentResult cr = serializeAnything(out, value, pMeta.getClassMeta(), p.getKey().toString(), pMeta, 2, false); 622 if (cr == CR_ELEMENTS) 623 out.i(i+2); 624 if (link != null) 625 out.eTag("a"); 626 out.eTag("td").nl(i+2); 627 } 628 } 629 } 630 out.ie(i+1).eTag("tr").nl(i+1); 631 } 632 out.ie(i).eTag("table").nl(i); 633 634 } else { 635 out.oTag(i, isDc ? "p" : "ul"); 636 if (! type2.equals("array")) 637 out.attr(btpn, type2); 638 out.append('>').nl(i+1); 639 boolean isFirst = true; 640 for (Object o : c) { 641 if (isDc && ! isFirst) 642 out.append(isCdc ? ", " : " "); 643 if (! isDc) 644 out.oTag(i+1, "li"); 645 String style = getStyle(this, ppMeta, o); 646 String link = getLink(ppMeta); 647 if (style != null && ! isDc) 648 out.attr("style", style); 649 if (! isDc) 650 out.cTag(); 651 if (link != null) 652 out.oTag(i+2, "a").attrUri("href", link.replace("{#}", asString(o))).cTag(); 653 ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, null, 1, false); 654 if (link != null) 655 out.eTag("a"); 656 if (cr == CR_ELEMENTS) 657 out.ie(i+1); 658 if (! isDc) 659 out.eTag("li").nl(i+1); 660 isFirst = false; 661 } 662 out.ie(i).eTag(isDc ? "p" : "ul").nl(i); 663 } 664 } 665 666 private static HtmlRender<?> getRender(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) { 667 if (pMeta == null) 668 return null; 669 HtmlRender<?> render = bpHtml(pMeta).getRender(); 670 if (render != null) 671 return render; 672 ClassMeta<?> cMeta = session.getClassMetaForObject(value); 673 render = cMeta == null ? null : cHtml(cMeta).getRender(); 674 return render; 675 } 676 677 @SuppressWarnings({"rawtypes","unchecked"}) 678 private static String getStyle(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) { 679 HtmlRender render = getRender(session, pMeta, value); 680 return render == null ? null : render.getStyle(session, value); 681 } 682 683 private static String getLink(BeanPropertyMeta pMeta) { 684 return pMeta == null ? null : pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class).getLink(); 685 } 686 687 private static String getAnchorText(BeanPropertyMeta pMeta) { 688 return pMeta == null ? null : pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class).getAnchorText(); 689 } 690 691 private static HtmlClassMeta cHtml(ClassMeta<?> cm) { 692 return cm.getExtendedMeta(HtmlClassMeta.class); 693 } 694 695 private static HtmlBeanPropertyMeta bpHtml(BeanPropertyMeta pMeta) { 696 return pMeta == null ? HtmlBeanPropertyMeta.DEFAULT : pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class); 697 } 698 699 /* 700 * Returns the table column headers for the specified collection of objects. 701 * Returns null if collection should not be serialized as a 2-dimensional table. 702 * 2-dimensional tables are used for collections of objects that all have the same set of property names. 703 */ 704 @SuppressWarnings({ "rawtypes", "unchecked" }) 705 private Object[] getTableHeaders(Collection c, HtmlBeanPropertyMeta bpHtml) throws Exception { 706 if (c.size() == 0) 707 return null; 708 c = sort(c); 709 Object[] th; 710 Set<ClassMeta> prevC = new HashSet<>(); 711 Object o1 = null; 712 for (Object o : c) 713 if (o != null) { 714 o1 = o; 715 break; 716 } 717 if (o1 == null) 718 return null; 719 ClassMeta<?> cm = getClassMetaForObject(o1); 720 721 PojoSwap swap = cm.getPojoSwap(this); 722 if (swap != null) { 723 o1 = swap.swap(this, o1); 724 cm = swap.getSwapClassMeta(this); 725 } 726 727 if (cm == null || ! cm.isMapOrBean()) 728 return null; 729 if (cm.getInnerClass().isAnnotationPresent(HtmlLink.class)) 730 return null; 731 732 HtmlClassMeta cHtml = cHtml(cm); 733 734 if (cHtml.isNoTables() || bpHtml.isNoTables()) 735 return null; 736 if (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders()) 737 return new Object[0]; 738 if (canIgnoreValue(cm, null, o1)) 739 return null; 740 if (cm.isMap() && ! cm.isBeanMap()) { 741 Set<Object> set = new LinkedHashSet<>(); 742 for (Object o : c) { 743 if (! canIgnoreValue(cm, null, o)) { 744 if (! cm.isInstance(o)) 745 return null; 746 Map m = sort((Map)o); 747 for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) { 748 if (e.getValue() != null) 749 set.add(e.getKey() == null ? null : e.getKey()); 750 } 751 } 752 } 753 th = set.toArray(new Object[set.size()]); 754 } else { 755 Map<String,Boolean> m = new LinkedHashMap<>(); 756 for (Object o : c) { 757 if (! canIgnoreValue(cm, null, o)) { 758 if (! cm.isInstance(o)) 759 return null; 760 BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : toBeanMap(o)); 761 for (Map.Entry<String,Object> e : bm.entrySet()) { 762 String key = e.getKey(); 763 if (e.getValue() != null) 764 m.put(key, true); 765 else if (! m.containsKey(key)) 766 m.put(key, false); 767 } 768 } 769 } 770 for (Iterator<Boolean> i = m.values().iterator(); i.hasNext();) 771 if (! i.next()) 772 i.remove(); 773 th = m.keySet().toArray(new Object[m.size()]); 774 } 775 prevC.add(cm); 776 boolean isSortable = true; 777 for (Object o : th) 778 isSortable &= (o instanceof Comparable); 779 Set<Object> s = (isSortable ? new TreeSet<>() : new LinkedHashSet<>()); 780 s.addAll(Arrays.asList(th)); 781 782 for (Object o : c) { 783 if (o == null) 784 continue; 785 cm = getClassMetaForObject(o); 786 787 PojoSwap ps = cm == null ? null : cm.getPojoSwap(this); 788 if (ps != null) { 789 o = ps.swap(this, o); 790 cm = ps.getSwapClassMeta(this); 791 } 792 if (prevC.contains(cm)) 793 continue; 794 if (cm == null || ! (cm.isMap() || cm.isBean())) 795 return null; 796 if (cm.getInnerClass().isAnnotationPresent(HtmlLink.class)) 797 return null; 798 if (canIgnoreValue(cm, null, o)) 799 return null; 800 if (cm.isMap() && ! cm.isBeanMap()) { 801 Map m = (Map)o; 802 if (th.length != m.keySet().size()) 803 return null; 804 for (Object k : m.keySet()) 805 if (! s.contains(k.toString())) 806 return null; 807 } else { 808 BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : toBeanMap(o)); 809 int l = 0; 810 for (String k : bm.keySet()) { 811 if (! s.contains(k)) 812 return null; 813 l++; 814 } 815 if (s.size() != l) 816 return null; 817 } 818 } 819 return th; 820 } 821 //----------------------------------------------------------------------------------------------------------------- 822 // Properties 823 //----------------------------------------------------------------------------------------------------------------- 824 825 /** 826 * Configuration property: Look for link labels in URIs. 827 * 828 * @see HtmlSerializer#HTML_detectLabelParameters 829 * @return 830 * <jk>true</jk> if we should look for URL label parameters (e.g. <js>"?label=foobar"</js>). 831 */ 832 protected final boolean isLookForLabelParameters() { 833 return ctx.isLookForLabelParameters(); 834 } 835 836 /** 837 * Configuration property: Look for URLs in {@link String Strings}. 838 * 839 * @see HtmlSerializer#HTML_detectLinksInStrings 840 * @return 841 * <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL. 842 */ 843 protected final boolean isDetectLinksInStrings() { 844 return ctx.isDetectLinksInStrings(); 845 } 846 847 /** 848 * Configuration property: Add key/value headers on bean/map tables. 849 * 850 * @see HtmlSerializer#HTML_addKeyValueTableHeaders 851 * @return 852 * <jk>true</jk> if <code><b>key</b></code> and <code><b>value</b></code> column headers are added to tables. 853 */ 854 protected final boolean isAddKeyValueTableHeaders() { 855 return ctx.isAddKeyValueTableHeaders(); 856 } 857 858 /** 859 * Configuration property: Add <js>"_type"</js> properties when needed. 860 * 861 * @see HtmlSerializer#HTML_addBeanTypes 862 * @return 863 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 864 * through reflection. 865 */ 866 @Override 867 protected final boolean isAddBeanTypes() { 868 return ctx.isAddBeanTypes(); 869 } 870 871 /** 872 * Configuration property: Link label parameter name. 873 * 874 * @see HtmlSerializer#HTML_labelParameter 875 * @return 876 * The parameter name to look for when resolving link labels via {@link HtmlSerializer#HTML_detectLabelParameters}. 877 */ 878 protected final String getLabelParameter() { 879 return ctx.getLabelParameter(); 880 } 881 882 /** 883 * Configuration property: Anchor text source. 884 * 885 * @see HtmlSerializer#HTML_uriAnchorText 886 * @return 887 * When creating anchor tags (e.g. <code><xt><a</xt> <xa>href</xa>=<xs>'...'</xs> 888 * <xt>></xt>text<xt></a></xt></code>) in HTML, this setting defines what to set the inner text to. 889 */ 890 protected final AnchorText getUriAnchorText() { 891 return ctx.getUriAnchorText(); 892 } 893}