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 * One entry of a card's {@code subscribes} list: a concrete topic wired to a role the card type implements.
025 *
026 * <h5 class='section'>Example:</h5>
027 * <p class='bjava'>
028 *   <jc>// Master/detail: the tasks table reloads with the selected change's id.</jc>
029 *   CardSpec <jv>tasks</jv> = CardSpec.<jsm>of</jsm>(<js>"datatables"</js>, <js>"tasks"</js>)
030 *     .subscribes(Subscription.<jsm>to</jsm>(Topics.<jsm>selection</jsm>(<js>"changes"</js>)).as(<js>"params"</js>)
031 *       .map(<js>"changeId"</js>, <js>"ids.0"</js>).whenEmpty(WhenEmpty.<jsf>CLEAR</jsf>)
032 *       .emptyText(<js>"Select a change to see its tasks."</js>));
033 *
034 *   <jc>// Linked filter, raw payload.</jc>
035 *   Subscription.<jsm>to</jsm>(Topics.<jsm>filter</jsm>(<js>"changes"</js>)).as(<js>"filter"</js>);
036 * </p>
037 *
038 * @since 10.0.0
039 */
040public final class Subscription {
041
042   /** What a subscriber does when the payload is cleared or a mapped path is missing. */
043   public enum WhenEmpty {
044      /** Paint {@code emptyText} and skip the fetch (the default). */
045      CLEAR,
046      /** Leave the last render alone. */
047      KEEP
048   }
049
050   private final String topic;
051   private String role;
052   private String emptyText;
053   private WhenEmpty whenEmpty;
054   private final Map<String,String> map = new LinkedHashMap<>();
055
056   private Subscription(String topic) {
057      this.topic = topic;
058   }
059
060   /**
061    * @param topic A concrete topic; no wildcards.
062    * @return A new subscription.
063    * @throws IllegalArgumentException E-40 on bad syntax.
064    */
065   public static Subscription to(String topic) {
066      Topics.checkTopic(topic);
067      return new Subscription(topic);
068   }
069
070   /** @param role A role the card type accepts, matching {@code ^[a-z][a-z0-9-]{0,31}$}. @return This object. */
071   public Subscription as(String role) {
072      if (role == null || ! Topics.ROLE.matcher(role).matches())
073         throw Topics.iae("subscription to '%s': role '%s' must match ^[a-z][a-z0-9-]{0,31}$", topic, role);
074      this.role = role;
075      return this;
076   }
077
078   /**
079    * Maps one payload path to a target; repeatable.
080    *
081    * @param target A {@code {token}} name for {@code params}, or a dotted field such as {@code columns.region} for {@code filter}.
082    * @param path A dotted path into the payload, with numeric segments for array indexes, such as {@code ids.0}.
083    * @return This object.
084    */
085   public Subscription map(String target, String path) {
086      if (target == null || ! Topics.MAP_PATH.matcher(target).matches())
087         throw Topics.iae("subscription to '%s': map target '%s' must be a dotted name", topic, target);
088      if (path == null || ! Topics.MAP_PATH.matcher(path).matches())
089         throw Topics.iae("subscription to '%s': map path '%s' must be a dotted path such as 'ids.0'", topic, path);
090      map.put(target, path);
091      return this;
092   }
093
094   /** @param v {@link WhenEmpty#CLEAR} (the default when unset) or {@link WhenEmpty#KEEP}. @return This object. */
095   public Subscription whenEmpty(WhenEmpty v) {
096      whenEmpty = Objects.requireNonNull(v, "whenEmpty");
097      return this;
098   }
099
100   /** @param v The text painted when the payload is empty; the shell's default otherwise. @return This object. */
101   public Subscription emptyText(String v) {
102      emptyText = v;
103      return this;
104   }
105
106   /** @return The topic. */
107   public String topic() {
108      return topic;
109   }
110
111   /** @return The {@code $defs/subscription} entry. */
112   public JsonMap toMap() {
113      if (role == null)
114         throw Topics.iae("subscription to '%s' needs as(role)", topic);
115      var m = new JsonMap();
116      m.put("topic", topic);
117      m.put("as", role);
118      if (! map.isEmpty()) {
119         var mm = new JsonMap();
120         mm.putAll(map);
121         m.put("map", mm);
122      }
123      if (whenEmpty != null)
124         m.put("whenEmpty", whenEmpty.name().toLowerCase(Locale.ROOT));
125      if (emptyText != null)
126         m.put("emptyText", emptyText);
127      return m;
128   }
129}