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.console;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022import java.util.function.*;
023
024import org.apache.juneau.marshall.collections.*;
025import org.apache.juneau.marshall.json5.*;
026import org.apache.juneau.marshall.parser.*;
027
028/**
029 * One authored card handed to a {@link CardTypeHandler}: its attributes, its raw body, and helpers to parse the
030 * body as JSON5 or capture it as a {@code <template data-card>}. Built by {@code <@card>} and by C3's
031 * {@code PageSpec}; tests build it directly.
032 *
033 * <h5 class='section'>Example:</h5>
034 * <p class='bjava'>
035 *   CardSource <jv>s</jv> = CardSource.<jsm>create</jsm>(<js>"datatables"</js>, <js>"releases"</js>)
036 *     .title(<js>"Releases"</js>)
037 *     .body(<js>"{dataUrl:'/rest/releases/data', columns:[{key:'version', label:'Version'}]}"</js>)
038 *     .build();
039 *   JsonMap <jv>frag</jv> = CardTypeRegistry.<jsm>standard</jsm>().handler(<js>"datatables"</js>).toFragment(<jv>s</jv>);
040 * </p>
041 *
042 * @since 10.0.0
043 */
044public final class CardSource {
045
046   private final String type;
047   private final String id;
048   private final String title;
049   private final String src;
050   private final String template;
051   private final String body;
052   private final Map<String,Object> bodyMap;
053   private final BiConsumer<String,String> templateSink;
054
055   private boolean jsonParsed;
056   private JsonMap cachedJson;
057
058   private CardSource(Builder b) {
059      type = b.type;
060      id = b.id;
061      title = b.title;
062      src = b.src;
063      template = b.template;
064      body = b.body == null ? null : b.body.trim();
065      bodyMap = b.bodyMap;
066      templateSink = b.templateSink;
067   }
068
069   /**
070    * Starts building a {@link CardSource}.
071    *
072    * @param type The card type name. Must not be <jk>null</jk> or blank.
073    * @param id The card id. Must not be <jk>null</jk> or blank.
074    * @return A new {@link Builder}.
075    * @throws IllegalArgumentException If {@code type} or {@code id} is <jk>null</jk> or blank.
076    */
077   public static Builder create(String type, String id) {
078      return new Builder(type, id);
079   }
080
081   /** @return The card type name. */
082   public String type() { return type; }
083
084   /** @return The card id. */
085   public String id() { return id; }
086
087   /** @return The card title, or <jk>null</jk>. */
088   public String title() { return title; }
089
090   /** @return The card's data-source URL, or <jk>null</jk>. */
091   public String src() { return src; }
092
093   /** @return The raw, trimmed body, or <jk>null</jk> when absent. */
094   public String body() { return body; }
095
096   /** @return The {@code template=} attribute value, or <jk>null</jk>. Package-private: an internal wiring detail. */
097   String template() { return template; }
098
099   /**
100    * @return <jk>true</jk> when {@link Builder#bodyMap(Map)} was used, or the raw body's first non-whitespace character
101    *    is an opening curly brace.
102    */
103   public boolean hasJsonBody() {
104      if (bodyMap != null)
105         return true;
106      return body != null && ! body.isEmpty() && body.charAt(0) == '{';
107   }
108
109   /**
110    * Parses the body as JSON5 (cached after the first call).
111    *
112    * @return The parsed body, or the {@link Builder#bodyMap(Map)} wrapped as a {@link JsonMap} when that was used.
113    * @throws IllegalArgumentException E-25 if the body is not JSON5 shaped; E-24 if it is but fails to parse.
114    */
115   public JsonMap json() {
116      if (jsonParsed)
117         return cachedJson;
118      jsonParsed = true;
119      if (bodyMap != null) {
120         cachedJson = new JsonMap();
121         cachedJson.putAll(bodyMap);
122         return cachedJson;
123      }
124      if (! hasJsonBody())
125         throw error("type='%s' requires a %s body; got '%s'.", type, "JSON5 object", preview());
126      try {
127         cachedJson = Json5Parser.DEFAULT.read(body, JsonMap.class);
128      } catch (ParseException ex) {
129         throw error("Card JSON5 is invalid: %s", ex.getMessage());
130      }
131      return cachedJson;
132   }
133
134   /** @return The pre-built map passed to {@link Builder#bodyMap(Map)} (the C3 path), or <jk>null</jk>. */
135   public Map<String,Object> bodyMap() { return bodyMap; }
136
137   /**
138    * Registers the raw body as a {@code <template data-card="{id}">} via the builder's {@link Builder#templateSink}
139    * and returns the card's own id as the template name.
140    *
141    * @return {@link #id()}.
142    * @throws IllegalArgumentException If the body is absent/blank, or no template sink was registered.
143    */
144   public String captureTemplate() {
145      if (ie(body))
146         throw error("type='%s' requires a markup body; got none.", type);
147      if (templateSink == null)
148         throw error("type='%s' requires a markup body, but this CardSource has no template sink.", type);
149      templateSink.accept(id, body);
150      return id;
151   }
152
153   /**
154    * Builds an {@link IllegalArgumentException} whose message is prefixed with {@code "<@card id='%s'> "}.
155    *
156    * @param fmt A {@link String#format(String, Object...)} pattern, using {@code %s} placeholders.
157    * @param args The format arguments.
158    * @return A new, unthrown exception (callers {@code throw} it themselves).
159    */
160   public IllegalArgumentException error(String fmt, Object... args) {
161      var all = new Object[args.length + 1];
162      all[0] = id;
163      System.arraycopy(args, 0, all, 1, args.length);
164      return iaex("<@card id='%s'> " + fmt, all);
165   }
166
167   private String preview() {
168      if (body == null)
169         return "";
170      return body.length() <= 40 ? body : body.substring(0, 40);
171   }
172
173   /** Builds a {@link CardSource}. */
174   public static final class Builder {
175      private final String type;
176      private final String id;
177      private String title;
178      private String src;
179      private String template;
180      private String body;
181      private Map<String,Object> bodyMap;
182      private BiConsumer<String,String> templateSink;
183
184      private Builder(String type, String id) {
185         if (ib(type))
186            throw iaex("CardSource type must not be null or blank.");
187         if (ib(id))
188            throw iaex("CardSource id must not be null or blank.");
189         this.type = type;
190         this.id = id;
191      }
192
193      /** @param v The card title. @return This object. */
194      public Builder title(String v) { title = v; return this; }
195
196      /** @param v The card's data-source URL. @return This object. */
197      public Builder src(String v) { src = v; return this; }
198
199      /** @param v The name of an existing {@code <template data-card>}. @return This object. */
200      public Builder template(String v) { template = v; return this; }
201
202      /** @param raw The raw card body (JSON5 or markup). @return This object. */
203      public Builder body(String raw) { body = raw; return this; }
204
205      /** @param m A pre-built map in place of a JSON5 body (the C3 path). @return This object. */
206      public Builder bodyMap(Map<String,Object> m) { bodyMap = m; return this; }
207
208      /** @param sink Receives {@code (id, markup)} when {@link CardSource#captureTemplate()} runs. @return This object. */
209      public Builder templateSink(BiConsumer<String,String> sink) { templateSink = sink; return this; }
210
211      /** @return The built {@link CardSource}. */
212      public CardSource build() { return new CardSource(this); }
213   }
214}