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}