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}