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.widgets;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.commons.http.*;
025
026/**
027 * An additive trailing region beside the subtab ribbon: a small row of {@link BarWidget}s (concept #9).
028 *
029 * <p>
030 * A pure data bean.  The views emitter renders a {@code data-juneau-bar-slot} region (never into the archived
031 * {@code .juneau-view-toolbar-*} DataTables control row) and, only when the slot is non-empty, a tiny JSON sidecar
032 * for any dynamic counts.  The remaining Java-only accessor is {@code RowDetailDef.barSlot(...)} in the views
033 * module &mdash; omitted from the wire, and it does not bump any {@code CONTRACT_VERSION}.
034 *
035 * @since 10.0.0
036 */
037@BeanType(properties="contractVersion,id,widgets")
038@SuppressWarnings({
039   "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
040})
041public class BarSlot implements Widget {
042
043   /** The frozen contract version for this widget.  Serialized as the JSON <b>string</b> {@code "1"}. */
044   public static final String CONTRACT_VERSION = "1";
045
046   /** The contract version; must equal {@link #CONTRACT_VERSION} at validation time. */
047   public String contractVersion = CONTRACT_VERSION;
048
049   /** The stable bar-slot id; sidecar id ({@code id="juneau-bar:<id>"}).  Required, non-blank. */
050   public String id;
051
052   /** The bar widgets, in display order.  At least one is required; ids must be unique within the slot. */
053   public List<BarWidget> widgets;
054
055   /** Optional same-origin refresh endpoint owning the bar sidecar's demand-refresh.  Omit = no live refresh. */
056   public String refreshUrl;
057
058   /**
059    * Creates an empty bar slot with the given id.
060    *
061    * @param id The stable bar-slot id.  Must not be <jk>null</jk> or blank.
062    * @return A new {@link BarSlot}.
063    */
064   public static BarSlot create(String id) {
065      var b = new BarSlot();
066      b.id = id;
067      return b;
068   }
069
070   /**
071    * Sets the bar widgets, in display order.
072    *
073    * @param value The widgets.
074    * @return This object.
075    */
076   public BarSlot widgets(BarWidget...value) {
077      widgets = l(value);
078      return this;
079   }
080
081   /**
082    * Sets the same-origin bar refresh endpoint.
083    *
084    * @param value The same-origin refresh path.
085    * @return This object.
086    */
087   public BarSlot refreshUrl(String value) {
088      refreshUrl = value;
089      return this;
090   }
091
092   @Override /* Widget */
093   public void validate() {
094      if (neq(contractVersion, CONTRACT_VERSION))
095         throw iaex("BarSlot contractVersion must be '%s': %s", CONTRACT_VERSION, contractVersion);
096      if (ib(id))
097         throw iaex("BarSlot id must not be null or blank.");
098      if (ie(widgets))
099         throw iaex("BarSlot '%s' must declare at least one widget.", id);
100      var ids = new HashSet<String>();
101      for (var w : widgets) {
102         if (w == null)
103            throw iaex("BarSlot '%s' widget must not be null.", id);
104         var wid = validateWidget(w);
105         if (!ids.add(wid))
106            throw iaex("BarSlot '%s' duplicate widget id '%s'.", id, wid);
107      }
108      if (refreshUrl != null && !SafePathTemplate.isSameOriginPath(refreshUrl))
109         throw iaex("BarSlot '%s' refreshUrl must be a same-origin path (no absolute URL, '//', scheme, "
110            + "'..', 'data:', or 'javascript:'): %s", id, refreshUrl);
111   }
112
113   /**
114    * Fail-closed per-widget validation, dispatching over the sealed {@link BarWidget}'s two permitted types.
115    *
116    * @param w The widget.
117    * @return The widget's id (for uniqueness checking).
118    */
119   private String validateWidget(BarWidget w) {
120      if (w instanceof BarBadge b) {
121         if (b.id == null || b.id.isBlank())
122            throw iaex("BarSlot '%s' BarBadge id must not be null or blank.", id);
123         if (b.badge != null)
124            b.badge.validate();
125         return b.id;
126      }
127      var t = (BarText)w;   // BarWidget is sealed to BarBadge and BarText, so this is the only remaining permit
128      if (t.id == null || t.id.isBlank())
129         throw iaex("BarSlot '%s' BarText id must not be null or blank.", id);
130      if (t.text == null || t.text.isBlank())
131         throw iaex("BarSlot '%s' BarText '%s' text must not be null or blank.", id, t.id);
132      return t.id;
133   }
134}