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.juneau.marshall.markdown; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.commons.bean.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.serializer.*; 027import org.apache.juneau.marshall.swap.spi.*; 028 029/** 030 * Session object for {@link MarkdownDocSerializer}. 031 * 032 * <h5 class='section'>Notes:</h5><ul> 033 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 034 * </ul> 035 * 036 */ 037@SuppressWarnings({ 038 "java:S110", // Inheritance depth acceptable for serializer session hierarchy 039 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 040 "java:S3740", // Raw ClassMeta locals in doWrite() and writeBeanWithHeadings() mirror MarkdownSerializerSession, where the swap API is used with raw ClassMeta 041 "java:S3776", // Cognitive complexity acceptable for doWrite / writeBeanWithHeadings 042 "java:S6541", // Brain method acceptable for doWrite 043 "rawtypes", // Raw ClassMeta locals (cm, swappedCm) in doWrite() and writeBeanWithHeadings() are passed to the ObjectSwap API, which is typed on the raw class 044 "resource" // MarkdownWriter/Writer lifecycle managed by SerializerPipe 045}) 046public class MarkdownDocSerializerSession extends MarkdownSerializerSession { 047 048 private final String title; 049 private final int headingLevel; 050 private final boolean addHorizontalRules; 051 private final String headerContent; 052 private final String footerContent; 053 054 /** 055 * Builder class. 056 */ 057 public static class Builder extends MarkdownSerializerSession.Builder<Builder> { 058 059 String title; 060 int headingLevel; 061 boolean addHorizontalRules; 062 String headerContent; 063 String footerContent; 064 065 /** 066 * Constructor. 067 * 068 * @param ctx The context creating this session. 069 * <br>Cannot be <jk>null</jk>. 070 */ 071 protected Builder(MarkdownDocSerializer ctx) { 072 super(reqnn("ctx", ctx)); 073 title = ctx.title; 074 headingLevel = ctx.headingLevel; 075 addHorizontalRules = ctx.addHorizontalRules; 076 headerContent = ctx.headerContent; 077 footerContent = ctx.footerContent; 078 } 079 080 @Override 081 public MarkdownDocSerializerSession build() { 082 return new MarkdownDocSerializerSession(this); 083 } 084 085 } 086 087 /** 088 * Creates a new builder for this object. 089 * 090 * @param ctx The context creating this session. 091 * <br>Cannot be <jk>null</jk>. 092 * @return A new builder. 093 */ 094 public static Builder create(MarkdownDocSerializer ctx) { 095 return new Builder(reqnn("ctx", ctx)); 096 } 097 098 /** 099 * Constructor. 100 * 101 * @param builder The builder for this object. 102 */ 103 protected MarkdownDocSerializerSession(Builder builder) { 104 super(builder); 105 title = builder.title; 106 headingLevel = builder.headingLevel; 107 addHorizontalRules = builder.addHorizontalRules; 108 headerContent = builder.headerContent; 109 footerContent = builder.footerContent; 110 } 111 112 @Override /* Overridden from MarkdownSerializerSession */ 113 protected void doWrite(SerializerPipe pipe, Object o) throws IOException, SerializeException { 114 try (var w = getMarkdownWriter(pipe)) { 115 if (ine(headerContent)) { 116 w.append(headerContent); 117 w.blankLine(); 118 } 119 120 if (ine(title)) { 121 w.heading(headingLevel, title); 122 w.blankLine(); 123 } 124 125 if (o == null) { 126 w.text(nullValue); 127 } else { 128 ClassMeta cm = getClassMetaForObject(o); 129 ObjectSwap swap = cm.getSwap(this); 130 if (swap != null) { 131 o = swap(swap, o); 132 cm = swap.getSwapClassMeta(this); 133 } 134 135 if (cm.isBean()) { 136 writeBeanWithHeadings(w, toBeanMap(o), headingLevel); 137 } else if (cm.isCollectionOrArray()) { 138 var l = cm.isArray() ? Arrays.asList((Object[]) o) : (Collection<?>) o; 139 writeCollection(w, l, object(), cm); 140 } else { 141 writeAnything(w, o, cm, null); 142 } 143 } 144 145 if (ine(footerContent)) { 146 w.blankLine(); 147 w.append(footerContent); 148 } 149 } 150 } 151 152 /** 153 * Serializes a bean in document mode, using headings to structure nested objects. 154 * 155 * <p> 156 * Simple properties go into a key/value table. Nested bean, map, and collection properties 157 * get their own sub-headings. 158 * 159 * @param w The writer. 160 * @param bm The bean map. 161 * @param level The current heading level. 162 * @throws IOException Thrown by underlying stream. 163 * @throws SerializeException If serialization fails. 164 */ 165 protected void writeBeanWithHeadings(MarkdownWriter w, BeanMap<?> bm, int level) throws IOException, SerializeException { 166 // Collect simple properties for the key/value table 167 var simpleProps = new LinkedHashMap<String,Object>(); 168 var complexProps = new LinkedHashMap<String,Object>(); 169 170 for (var e : bm.entrySet()) { 171 var key = e.getKey(); 172 var val = e.getValue(); 173 if (val == null) { 174 simpleProps.put(key, null); 175 } else { 176 ClassMeta cm = getClassMetaForObject(val); 177 ObjectSwap swap = cm.getSwap(this); 178 Object swapped = val; 179 ClassMeta swappedCm = cm; 180 if (swap != null) { 181 swapped = swap(swap, val); 182 swappedCm = swap.getSwapClassMeta(this); 183 } 184 if (swappedCm.isBean() || swappedCm.isMap() || swappedCm.isCollectionOrArray() || swappedCm.isStreamable()) { 185 complexProps.put(key, swapped); 186 } else { 187 simpleProps.put(key, val); 188 } 189 } 190 } 191 192 // Render simple properties as a key/value table 193 if (!simpleProps.isEmpty()) { 194 if (showHeaders) { 195 w.tableHeader("Property", "Value"); 196 w.tableSeparator(2); 197 } 198 for (var e : simpleProps.entrySet()) 199 w.tableRow(MarkdownWriter.escapeCell(e.getKey()), writeInlineValue(e.getValue())); 200 w.blankLine(); 201 } 202 203 // Render complex properties as sub-sections 204 for (var e : complexProps.entrySet()) { 205 var key = e.getKey(); 206 var val = e.getValue(); 207 208 if (addHorizontalRules) 209 w.horizontalRule(); 210 211 w.heading(level + 1, key); 212 w.blankLine(); 213 214 ClassMeta cm = getClassMetaForObject(val); 215 ObjectSwap swap = cm.getSwap(this); 216 Object swapped = val; 217 ClassMeta swappedCm = cm; 218 if (swap != null) { 219 swapped = swap(swap, val); 220 swappedCm = swap.getSwapClassMeta(this); 221 } 222 223 if (swappedCm.isBean()) { 224 if (level + 1 < 6) { 225 writeBeanWithHeadings(w, toBeanMap(swapped), level + 1); 226 } else { 227 // Heading cap: fall back to inline rendering 228 writeBeanMap(w, toBeanMap(swapped), object(), swappedCm); 229 w.blankLine(); 230 } 231 } else if (swappedCm.isMap()) { 232 writeMap(w, (Map<?,?>) swapped, swappedCm); 233 w.blankLine(); 234 } else if (swappedCm.isCollectionOrArray()) { 235 var l = swappedCm.isArray() ? Arrays.asList((Object[]) swapped) : (Collection<?>) swapped; 236 writeCollection(w, l, object(), swappedCm); 237 w.blankLine(); 238 } else { 239 writeAnything(w, val, swappedCm, null); 240 w.blankLine(); 241 } 242 } 243 } 244}