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 — 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}