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.regex.*;
023
024/**
025 * Builds framework topic names so callers do not hand-format "family:key" strings.
026 *
027 * <h5 class='section'>Example:</h5>
028 * <p class='bjava'>
029 *   Topics.<jsm>selection</jsm>(<js>"changes"</js>);   <jc>// "selection:changes"</jc>
030 *   Topics.<jsm>cmd</jsm>(<js>"tasks"</js>);           <jc>// "cmd:tasks"</jc>
031 *   Topics.<jsm>job</jsm>(<jv>ref</jv>.id());          <jc>// "job:j-91"</jc>
032 * </p>
033 *
034 * @since 10.0.0
035 */
036@SuppressWarnings({
037   "java:S1192" // Repeated literals are contract wire keys; named constants would obscure the contract shape.
038})
039public final class Topics {
040
041   /** The framework families, in the order {@code JuneauViews.bus.FRAMEWORK_FAMILIES} lists them (pinned by a test). */
042   public static final Set<String> FRAMEWORK_FAMILIES = Collections.unmodifiableSet(new LinkedHashSet<>(List.of(
043      "card", "selection", "filter", "redraw", "detail", "bulk", "cmd", "probe", "job", "badge", "bridge", "data", "zoom", "legend")));
044
045   // The grammar.  One copy: Subscription, TopicDecl, BridgeDecl and BusWiringValidator read these, and
046   // Topics_Test pins them against juneau-page.schema.json.
047   private static final String CUSTOM = "[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,31}";
048   private static final String KEY = "[A-Za-z0-9_.-]{1,128}";
049   static final Pattern TOPIC = Pattern.compile("^(" + String.join("|", FRAMEWORK_FAMILIES) + "|" + CUSTOM + ")(:" + KEY + ")?$");
050   static final Pattern TOPIC_DECL = Pattern.compile("^(" + CUSTOM + "(:(" + KEY + "|\\*))?|job:\\*|data:" + KEY + ")$");
051   static final Pattern PUBLICATION = Pattern.compile("^(" + CUSTOM + "(:(" + KEY + "|\\*))?|(zoom|legend):" + KEY + ")$");
052   /** {@link #PUBLICATION} without the chart state families: what BusWiringValidator treats as a custom declaration. */
053   static final Pattern CUSTOM_PUBLICATION = Pattern.compile("^" + CUSTOM + "(:(" + KEY + "|\\*))?$");
054   /** Framework families a declaration may name (with a concrete key): {@code data} page-level, {@code zoom}/{@code legend} on a card. */
055   static final Set<String> DECLARABLE_FRAMEWORK = Set.of("data", "zoom", "legend");
056   static final Pattern ROLE = Pattern.compile("^[a-z][a-z0-9-]{0,31}$");
057   static final Pattern MAP_PATH = Pattern.compile("^[A-Za-z0-9_-]++(?:\\.[A-Za-z0-9_-]++)*+$");
058   /** Builder-time bridge session: same-origin path (no {@code //} or {@code /\}) or a {@code servlet:} / {@code context:} URI resolved at render. */
059   static final Pattern SESSION = Pattern.compile("^(/(?![/\\\\])|servlet:/|context:/)\\S*$");
060   /** The card and dashboard-section id grammar ({@code $defs/id} in the page schema). */
061   public static final Pattern ID = Pattern.compile("^[A-Za-z][A-Za-z0-9_-]{0,63}$");
062   private static final Pattern KEY_ONLY = Pattern.compile("^" + KEY + "$");
063
064   static final String E40 = "invalid topic '%s': expected family[:key] (see the topic syntax)";
065   static final String E45_FRAMEWORK = "'%s' is a framework family; framework topics are implicit and may not be declared";
066   static final String E45_NAMESPACE = "custom topic family '%s' must be namespaced (e.g. 'app.%s')";
067
068   private Topics() {}
069
070   /** @param cardId The card id. @return {@code card:<cardId>}. */
071   public static String card(String cardId) { return framework("card", cardId); }
072   /** @param cardId The card id. @return {@code selection:<cardId>}. */
073   public static String selection(String cardId) { return framework("selection", cardId); }
074   /** @param cardId The card id. @return {@code filter:<cardId>}. */
075   public static String filter(String cardId) { return framework("filter", cardId); }
076   /** @param cardId The card id. @return {@code redraw:<cardId>}. */
077   public static String redraw(String cardId) { return framework("redraw", cardId); }
078   /** @param cardId The card id. @return {@code detail:<cardId>}. */
079   public static String detail(String cardId) { return framework("detail", cardId); }
080   /** @param cardId The card id. @return {@code bulk:<cardId>}. */
081   public static String bulk(String cardId) { return framework("bulk", cardId); }
082   /** @param cardId The card id. @return {@code cmd:<cardId>}. */
083   public static String cmd(String cardId) { return framework("cmd", cardId); }
084   /** @param groupId The probe group id. @return {@code probe:<groupId>}. */
085   public static String probe(String groupId) { return framework("probe", groupId); }
086   /** @param jobId The async job id. @return {@code job:<jobId>}. */
087   public static String job(String jobId) { return framework("job", jobId); }
088   /** @param badgeId The badge id. @return {@code badge:<badgeId>}. */
089   public static String badge(String badgeId) { return framework("badge", badgeId); }
090   /** @param bridgeId The bridge id. @return {@code bridge:<bridgeId>}. */
091   public static String bridge(String bridgeId) { return framework("bridge", bridgeId); }
092   /** @param source The data source name. @return {@code data:<source>}. */
093   public static String data(String source) { return framework("data", source); }
094   /** @param cardId The chart card id. @return {@code zoom:<cardId>}. */
095   public static String zoom(String cardId) { return framework("zoom", cardId); }
096   /** @param cardId The chart card id. @return {@code legend:<cardId>}. */
097   public static String legend(String cardId) { return framework("legend", cardId); }
098
099   /**
100    * @param ns The namespace, for example {@code ssc}.
101    * @param name The name within it, for example {@code focus}.
102    * @return {@code ns.name}.
103    * @throws IllegalArgumentException E-45 when {@code ns} is blank; E-40 when a part is malformed.
104    */
105   public static String custom(String ns, String name) {
106      if (ib(ns))
107         throw iae(E45_NAMESPACE, name, name);
108      var t = ns + "." + name;
109      if (! TOPIC_DECL.matcher(t).matches())
110         throw iae(E40, t);
111      return t;
112   }
113
114   /**
115    * @param ns The namespace.
116    * @param name The name within it.
117    * @param key The key, or {@code *} for a declaration pattern.
118    * @return {@code ns.name:key}.
119    * @throws IllegalArgumentException E-45 / E-40 as for {@link #custom(String, String)}, or E-40 on a bad key.
120    */
121   public static String custom(String ns, String name, String key) {
122      var t = custom(ns, name) + ":" + key;
123      if (! TOPIC_DECL.matcher(t).matches())
124         throw iae(E40, t);
125      return t;
126   }
127
128   /**
129    * Tests the topic-key grammar (the part after {@code family:}).
130    *
131    * @param key The key, or <jk>null</jk>.
132    * @return <jk>true</jk> if {@code key} is a valid topic key.
133    */
134   public static boolean isKey(String key) {
135      return key != null && KEY_ONLY.matcher(key).matches();
136   }
137
138   private static String framework(String family, String key) {
139      var t = family + ":" + key;
140      if (key == null || ! KEY_ONLY.matcher(key).matches())
141         throw iae(E40, t);
142      return t;
143   }
144
145   /** Throws E-40 unless {@code topic} is a concrete topic. */
146   static void checkTopic(String topic) {
147      if (topic == null || ! TOPIC.matcher(topic).matches())
148         throw iae(E40, topic);
149   }
150
151   /** @return The text before the first {@code :}, or the whole topic. */
152   static String family(String topic) {
153      var i = topic.indexOf(':');
154      return i < 0 ? topic : topic.substring(0, i);
155   }
156
157   static IllegalArgumentException iae(String format, Object... args) {
158      return new IllegalArgumentException(f(format, args));
159   }
160}