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}