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 java.util.*;
020
021import org.apache.juneau.marshall.collections.*;
022
023/**
024 * A custom topic declaration, for a card's {@code publishes} or the page's top-level {@code topics}.
025 *
026 * <h5 class='section'>Example:</h5>
027 * <p class='bjava'>
028 *   <jc>// Published by this card's JS:</jc>
029 *   <jv>card</jv>.publishes(TopicDecl.<jsm>of</jsm>(<js>"ssc.focus"</js>).retain(<jk>true</jk>));
030 *
031 *   <jc>// Published by a page script or the server:</jc>
032 *   PageSpec.<jsm>create</jsm>().topic(TopicDecl.<jsm>of</jsm>(<js>"app.region-picked"</js>).retain(<jk>true</jk>)
033 *     .publisher(TopicDecl.Publisher.<jsf>SCRIPT</jsf>));
034 * </p>
035 *
036 * @since 10.0.0
037 */
038public final class TopicDecl {
039
040   /** Who publishes a page-level topic. */
041   public enum Publisher {
042      /** A page script. */
043      SCRIPT,
044      /** The server, through a bridge (R-11). */
045      SERVER,
046      /** Only ribbon {@code publish} items. */
047      RIBBON
048   }
049
050   private static final String JOB_ALL = "job:*";
051
052   private final String topic;
053   private Boolean retain;
054   private Publisher publisher;
055
056   private TopicDecl(String topic) {
057      this.topic = topic;
058   }
059
060   /**
061    * @param topic A custom topic or {@code ns.name:*}; or {@code job:*}, which is only valid page-level with {@link Publisher#SERVER}.
062    * @return A new declaration.
063    * @throws IllegalArgumentException E-45 for a framework family or an un-namespaced family; E-40 on bad syntax.
064    */
065   public static TopicDecl of(String topic) {
066      if (topic == null)
067         throw Topics.iae(Topics.E40, topic);
068      var family = Topics.family(topic);
069      var declarable = Topics.DECLARABLE_FRAMEWORK.contains(family);
070      if (Topics.FRAMEWORK_FAMILIES.contains(family) && ! JOB_ALL.equals(topic) && ! declarable)
071         throw Topics.iae(Topics.E45_FRAMEWORK, family);
072      if (Topics.ROLE.matcher(family).matches() && ! Topics.FRAMEWORK_FAMILIES.contains(family))
073         throw Topics.iae(Topics.E45_NAMESPACE, family, family);
074      if (declarable ? ! Topics.TOPIC.matcher(topic).matches() || topic.indexOf(':') < 0
075            : ! Topics.TOPIC_DECL.matcher(topic).matches())
076         throw Topics.iae(Topics.E40, topic);
077      return new TopicDecl(topic);
078   }
079
080   /** @param v Whether the bus keeps the last value for late subscribers. Required. @return This object. */
081   public TopicDecl retain(boolean v) {
082      retain = v;
083      return this;
084   }
085
086   /** @param p Who publishes the topic. Page-level only. @return This object. */
087   public TopicDecl publisher(Publisher p) {
088      publisher = Objects.requireNonNull(p, "publisher");
089      return this;
090   }
091
092   /** @return The topic. */
093   public String topic() {
094      return topic;
095   }
096
097   /** @return The page-level {@code $defs/topicDecl} entry. */
098   public JsonMap toMap() {
099      if (JOB_ALL.equals(topic) && publisher != Publisher.SERVER)
100         throw Topics.iae(Topics.E45_FRAMEWORK, "job");
101      var family = Topics.family(topic);
102      if ("zoom".equals(family) || "legend".equals(family))
103         throw Topics.iae("topic '%s': %s:<cardId> is published by its chart card; declare it with publishZoom()/publishLegend()", topic, family);
104      if ("data".equals(family) && (publisher != Publisher.SCRIPT || ! Boolean.TRUE.equals(retain)))
105         throw Topics.iae("topic '%s': a page-declared data: topic needs publisher(SCRIPT) and retain(true)", topic);
106      var m = base();
107      if (publisher == null)
108         throw Topics.iae("topic '%s': a page-level topic needs publisher(SCRIPT|SERVER|RIBBON)", topic);
109      m.put("publisher", publisher.name().toLowerCase(Locale.ROOT));
110      return m;
111   }
112
113   /** @return The card-level {@code $defs/publication} entry, for {@code CardSpec.publishes}. */
114   public JsonMap toPublicationMap() {
115      if (JOB_ALL.equals(topic))
116         throw Topics.iae(Topics.E45_FRAMEWORK, "job");
117      if ("data".equals(Topics.family(topic)))
118         throw Topics.iae("topic '%s': data: topics are declared page-level (publisher SCRIPT) or by a data source, not by a card", topic);
119      if (Topics.DECLARABLE_FRAMEWORK.contains(Topics.family(topic)) && ! Boolean.TRUE.equals(retain))
120         throw Topics.iae("topic '%s' is chart state; declare it with retain(true)", topic);
121      if (publisher != null)
122         throw Topics.iae("topic '%s': publisher is page-level only; a card's publishes may not set it", topic);
123      return base();
124   }
125
126   private JsonMap base() {
127      if (retain == null)
128         throw Topics.iae("topic '%s' is declared with retain=unset here; call retain(true) or retain(false)", topic);
129      var m = new JsonMap();
130      m.put("topic", topic);
131      m.put("retain", retain);
132      return m;
133   }
134}