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.freemarker; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.f; 021 022import java.util.*; 023 024import org.apache.juneau.marshall.collections.*; 025import org.apache.juneau.console.*; 026 027/** 028 * A section of a dashboard {@link Rail}. 029 * 030 * <p> 031 * Every section except a {@link CardSection} owns one {@code filter:<key>} topic, set with {@link #publishes(String)}. 032 * The key is the section's id and its bus owner id. The section publishes a filter-shaped payload 033 * ({@code {schemaVersion:1, viewId, search?, columns?, options?}}) whenever the user changes it. 034 * </p> 035 * 036 * <h5 class='section'>Example:</h5> 037 * <p class='bjava'> 038 * Rail.<jsm>left</jsm>().sections( 039 * SearchSection.<jsm>create</jsm>().publishes(Topics.<jsm>filter</jsm>(<js>"search"</js>)), 040 * Section.<jsm>custom</jsm>(<js>"app.goto"</js>, JsonMap.<jsm>of</jsm>()).publishes(Topics.<jsm>filter</jsm>(<js>"goto"</js>))); 041 * </p> 042 * 043 * @since 10.0.0 044 */ 045public abstract class Section { 046 047 String topic; 048 String label; 049 050 Section() {} 051 052 /** 053 * Sets the topic this section owns. 054 * 055 * @param topic A {@code filter:<key>} topic, normally {@code Topics.filter(key)}. {@code <key>} becomes the section id. 056 * @return This object. 057 */ 058 public Section publishes(String topic) { 059 this.topic = topic; 060 return this; 061 } 062 063 /** 064 * Sets the section's legend text. 065 * 066 * @param label The label. 067 * @return This object. 068 */ 069 public Section label(String label) { 070 this.label = label; 071 return this; 072 } 073 074 /** 075 * Creates a section implemented in JS by a {@code JuneauDashboard.section.register(name, ...)} section. 076 * 077 * @param name The dotted section name, for example {@code app.goto}. 078 * @param args Arguments handed to the section's {@code mount} as {@code ctx.args}. 079 * @return A new section. 080 */ 081 public static Custom custom(String name, JsonMap args) { 082 return new Custom(name, args); 083 } 084 085 /** @return The section id (the key of its topic), or <jk>null</jk> before {@link #publishes(String)} or for a card section. */ 086 String id() { 087 return topic == null ? null : topic.substring(topic.indexOf(':') + 1); 088 } 089 090 /** @return The {@code kind} value of the section JSON. */ 091 abstract String kind(); 092 093 /** @return A human name for messages, for example {@code "range section"}. */ 094 String what() { 095 return kind() + " section"; 096 } 097 098 /** Adds the kind-specific fields. */ 099 abstract void body(Map<String,Object> m); 100 101 /** @return The cards this section adds to the page (only a card section has one). */ 102 List<CardSpec> cards() { 103 return List.of(); 104 } 105 106 /** 107 * Checks this section, reporting E-77 under {@code dash}. 108 * 109 * @param dash The dashboard id. 110 */ 111 void check(String dash) { 112 if (topic == null) 113 throw e77(dash, "a " + what() + " must call publishes(Topics.filter(...))"); 114 if (! topic.startsWith("filter:")) 115 throw e77(dash, "section topic '" + topic + "' is not a filter: topic"); 116 if (! Topics.ID.matcher(id()).matches()) 117 throw e77(dash, "section id '" + id() + "' is not a valid id"); 118 } 119 120 /** @return The section JSON. */ 121 Map<String,Object> toMap() { 122 Map<String,Object> m = map("id", id(), "kind", kind(), "topic", topic); 123 if (label != null) 124 m.put("label", label); 125 body(m); 126 return m; 127 } 128 129 /** 130 * Builds an E-77 exception. 131 * 132 * @param dash The dashboard id. 133 * @param detail The problem. A format argument, so a {@code %} in it is harmless. 134 * @return The exception, for the caller to throw. 135 */ 136 static IllegalArgumentException e77(String dash, String detail) { 137 return new IllegalArgumentException("E-77: " + f(BusWiringValidator.E77, dash, detail)); 138 } 139 140 /** 141 * Rejects a bare card as a dashboard member: the shell renders a bare card without a host element to place. 142 * 143 * @param dash The dashboard id. 144 * @param card The panel or card-section card. 145 */ 146 static void checkMemberCard(String dash, CardSpec card) { 147 if (Boolean.TRUE.equals(card.toMap().get("bare"))) 148 throw e77(dash, "card '" + card.id() + "' is bare; a dashboard member needs a card host"); 149 } 150 151 /** 152 * Checks a card the dashboard itself adds to the page: {@link #checkMemberCard}, plus the types that 153 * {@code PageSpec.card} refuses because they have dedicated methods. 154 * 155 * @param dash The dashboard id. 156 * @param card The card. 157 */ 158 static void checkOwnedCard(String dash, CardSpec card) { 159 if ("html".equals(card.type()) || "datatables".equals(card.type())) 160 throw e77(dash, "card '" + card.id() + "' has type '" + card.type() + "', which has a dedicated PageSpec method; add it with PageSpec." 161 + ("html".equals(card.type()) ? "html" : "table") + "(...) and use panel(\"" + card.id() + "\")"); 162 checkMemberCard(dash, card); 163 } 164 165 /** @return <jk>true</jk> if {@code name} is a dotted extension name (the E-74 shape). */ 166 @SuppressWarnings({ 167 "java:S2692" // A leading dot is deliberately invalid, so indexOf must be > 0, not >= 0. 168 }) 169 static boolean isDottedName(String name) { 170 return name != null && name.indexOf('.') > 0 && BusWiringValidator.EXTENSION_NAME.matcher(name).matches(); 171 } 172 173 /** 174 * A section implemented in JS. 175 * 176 * <h5 class='section'>Example:</h5> 177 * <p class='bjava'> 178 * Section.<jsm>custom</jsm>(<js>"app.goto"</js>, JsonMap.<jsm>of</jsm>()).publishes(Topics.<jsm>filter</jsm>(<js>"goto"</js>)); 179 * </p> 180 * 181 * @since 10.0.0 182 */ 183 public static final class Custom extends Section { 184 185 private final String name; 186 private final JsonMap args; 187 188 Custom(String name, JsonMap args) { 189 this.name = name; 190 this.args = args == null ? JsonMap.of() : new JsonMap(args); 191 } 192 193 @Override /* Section */ 194 public Custom publishes(String topic) { 195 super.publishes(topic); 196 return this; 197 } 198 199 @Override /* Section */ 200 public Custom label(String label) { 201 super.label(label); 202 return this; 203 } 204 205 @Override String kind() { return "custom"; } 206 207 @Override void check(String dash) { 208 if (! isDottedName(name)) 209 throw e77(dash, "custom section name '" + name + "' is not a dotted extension name"); 210 super.check(dash); 211 } 212 213 @Override void body(Map<String,Object> m) { 214 m.put("name", name); 215 m.put("args", args); 216 } 217 } 218}