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