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.commons.svl;
018
019import java.util.*;
020
021/**
022 * A typed, structured value (list, block or table) that an SVL expression function can return.
023 *
024 * <p>
025 * The core never emits markup.  {@link #toString()} gives a plain-text fallback; a layer that understands richer
026 * output (such as the J0439 template layer) interprets {@link #kind()}, {@link #items()} / {@link #rows()} and
027 * {@link #options()} itself.
028 *
029 * <p>
030 * Instances are immutable; all inputs are defensively copied (shallow: the elements themselves are not copied).
031 */
032public final class Structure {
033
034   /** The kind of structure. */
035   public enum Kind {
036      /** Unordered list. */ UL,
037      /** Ordered list. */ OL,
038      /** A single block of content. */ BLOCK,
039      /** A table with column headers and rows. */ TABLE
040   }
041
042   private final Kind kind;
043   private final List<?> items;
044   private final List<String> columns;
045   private final List<List<?>> rows;
046   private final Map<String,Object> options;
047
048   private Structure(Kind kind, List<?> items, List<String> columns, List<List<?>> rows, Map<String,Object> options) {
049      this.kind = kind;
050      this.items = items;
051      this.columns = columns;
052      this.rows = rows;
053      this.options = options == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(options));
054   }
055
056   /**
057    * Creates an unordered list.
058    *
059    * @param items The items.  Must not be <jk>null</jk>.
060    * @return A new structure.
061    */
062   public static Structure ul(List<?> items) { return ul(items, null); }
063
064   /**
065    * Creates an unordered list with options.
066    *
067    * @param items The items.  Must not be <jk>null</jk>.
068    * @param options Layer-defined options.  May be <jk>null</jk>.
069    * @return A new structure.
070    */
071   public static Structure ul(List<?> items, Map<String,Object> options) {
072      return new Structure(Kind.UL, copy(items), null, null, options);
073   }
074
075   /**
076    * Creates an ordered list.
077    *
078    * @param items The items.  Must not be <jk>null</jk>.
079    * @return A new structure.
080    */
081   public static Structure ol(List<?> items) { return ol(items, null); }
082
083   /**
084    * Creates an ordered list with options.
085    *
086    * @param items The items.  Must not be <jk>null</jk>.
087    * @param options Layer-defined options.  May be <jk>null</jk>.
088    * @return A new structure.
089    */
090   public static Structure ol(List<?> items, Map<String,Object> options) {
091      return new Structure(Kind.OL, copy(items), null, null, options);
092   }
093
094   /**
095    * Creates a block holding a single content item.
096    *
097    * @param content The content.  May be <jk>null</jk>.
098    * @return A new structure.
099    */
100   public static Structure block(Object content) { return block(content, null); }
101
102   /**
103    * Creates a block with options.
104    *
105    * @param content The content.  May be <jk>null</jk>.
106    * @param options Layer-defined options.  May be <jk>null</jk>.
107    * @return A new structure.
108    */
109   public static Structure block(Object content, Map<String,Object> options) {
110      var l = new ArrayList<Object>(1);
111      l.add(content);
112      return new Structure(Kind.BLOCK, Collections.unmodifiableList(l), null, null, options);
113   }
114
115   /**
116    * Creates a table.
117    *
118    * @param columns The column headers.  Must not be <jk>null</jk>.
119    * @param rows The rows.  Must not be <jk>null</jk>.
120    * @return A new structure.
121    */
122   public static Structure table(List<String> columns, List<List<?>> rows) { return table(columns, rows, null); }
123
124   /**
125    * Creates a table with options.
126    *
127    * @param columns The column headers.  Must not be <jk>null</jk>.
128    * @param rows The rows.  Must not be <jk>null</jk>.
129    * @param options Layer-defined options.  May be <jk>null</jk>.
130    * @return A new structure.
131    */
132   public static Structure table(List<String> columns, List<List<?>> rows, Map<String,Object> options) {
133      Objects.requireNonNull(rows, "rows");
134      var r = new ArrayList<List<?>>(rows.size());
135      for (var row : rows)
136         r.add(copy(Objects.requireNonNull(row, "rows must not contain a null row")));
137      return new Structure(Kind.TABLE, null, Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(columns, "columns"))), Collections.unmodifiableList(r), options);
138   }
139
140   private static List<?> copy(List<?> l) {
141      return Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(l, "items")));
142   }
143
144   /**
145    * Returns the kind of this structure.
146    *
147    * @return The kind.
148    */
149   public Kind kind() { return kind; }
150
151   /**
152    * Returns the items of a list or block.
153    *
154    * @return An unmodifiable list, or an empty list for a table.
155    */
156   @SuppressWarnings({
157      "java:S1452" // Items are heterogeneous by design; the wildcard is the public contract
158   })
159   public List<?> items() { return items == null ? List.of() : items; }
160
161   /**
162    * Returns the column headers of a table.
163    *
164    * @return An unmodifiable list, or an empty list for a non-table.
165    */
166   public List<String> columns() { return columns == null ? List.of() : columns; }
167
168   /**
169    * Returns the rows of a table.
170    *
171    * @return An unmodifiable list, or an empty list for a non-table.
172    */
173   public List<List<?>> rows() { return rows == null ? List.of() : rows; }
174
175   /**
176    * Returns the layer-defined options.
177    *
178    * @return An unmodifiable map; never <jk>null</jk>.
179    */
180   public Map<String,Object> options() { return options; }
181
182   /**
183    * Plain-text fallback.
184    *
185    * <p>
186    * Lists render one item per line prefixed with <c>- </c> or <c>N. </c>; a block renders its content; a table renders
187    * the header and then each row as tab-separated values, one per line.
188    */
189   @Override
190   public String toString() {
191      var sb = new StringBuilder();
192      switch (kind) {
193         case UL -> {
194            for (var o : items)
195               line(sb, "- " + o);
196         }
197         case OL -> {
198            var n = 1;
199            for (var o : items)
200               line(sb, (n++) + ". " + o);
201         }
202         case BLOCK -> sb.append(items.get(0));
203         case TABLE -> {
204            line(sb, String.join("\t", columns));
205            for (var r : rows) {
206               var j = new StringJoiner("\t");
207               for (var o : r)
208                  j.add(String.valueOf(o));
209               line(sb, j.toString());
210            }
211         }
212      }
213      return sb.toString();
214   }
215
216   private static void line(StringBuilder sb, String s) {
217      if (! sb.isEmpty())
218         sb.append('\n');
219      sb.append(s);
220   }
221}