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}