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}