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