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 * Declares one server bridge in the page contract's {@code bridges} list.
025 *
026 * <h5 class='section'>Example:</h5>
027 * <p class='bjava'>
028 *   PageSpec.<jsm>create</jsm>()
029 *     .topic(TopicDecl.<jsm>of</jsm>(<js>"ops.jobs"</js>).retain(<jk>true</jk>).publisher(TopicDecl.Publisher.<jsf>SERVER</jsf>))
030 *     .bridge(BridgeDecl.<jsm>sse</jsm>(<js>"ops"</js>, <js>"servlet:/juneau-bus/session"</js>).downstream(<js>"ops.jobs"</js>, Topics.<jsm>cmd</jsm>(<js>"jobs"</js>)));
031 *
032 *   <jc>// WebSocket, with an upstream topic a ribbon item publishes:</jc>
033 *   BridgeDecl.<jsm>websocket</jsm>(<js>"ops"</js>, <js>"servlet:/juneau-bus/session"</js>)
034 *     .downstream(<js>"ops.jobs"</js>).upstream(<js>"ops.cancel-all"</js>);
035 * </p>
036 *
037 * @since 10.0.0
038 */
039public final class BridgeDecl {
040
041   private final String id;
042   private final String transport;
043   private final String session;
044   private final List<String> downstream = new ArrayList<>();
045   private final List<String> upstream = new ArrayList<>();
046   private Integer maxAttempts;
047
048   private BridgeDecl(String id, String transport, String session) {
049      if (id == null || ! Topics.ID.matcher(id).matches())
050         throw e51(id, "id must match ^[A-Za-z][A-Za-z0-9_-]{0,63}$");
051      if (session == null || ! Topics.SESSION.matcher(session).matches())
052         throw e51(id, "session '" + session + "' must be a same-origin path or a servlet:/context: URI");
053      this.id = id;
054      this.transport = transport;
055      this.session = session;
056   }
057
058   /** @param id The bridge id. @param sessionUrl The session-POST URL. @return A new downstream-only SSE bridge. */
059   public static BridgeDecl sse(String id, String sessionUrl) {
060      return new BridgeDecl(id, "sse", sessionUrl);
061   }
062
063   /** @param id The bridge id. @param sessionUrl The session-POST URL. @return A new WebSocket bridge. */
064   public static BridgeDecl websocket(String id, String sessionUrl) {
065      return new BridgeDecl(id, "websocket", sessionUrl);
066   }
067
068   /** @param topicPatterns Topics the page accepts from the server; appends. @return This object. */
069   public BridgeDecl downstream(String... topicPatterns) {
070      for (var t : topicPatterns) {
071         if (t == null || ! (Topics.TOPIC.matcher(t).matches() || Topics.TOPIC_DECL.matcher(t).matches()))
072            throw Topics.iae(Topics.E40, t);
073         downstream.add(t);
074      }
075      return this;
076   }
077
078   /** @param topics Custom topics forwarded to the server; appends. WebSocket only. @return This object. */
079   public BridgeDecl upstream(String... topics) {
080      if (! "websocket".equals(transport))
081         throw e51(id, "upstream is allowed only on a websocket bridge");
082      for (var t : topics) {
083         Topics.checkTopic(t);
084         upstream.add(t);
085      }
086      return this;
087   }
088
089   /** @param v Give up after this many failed attempts (at least 1); unset retries while the page is open. @return This object. */
090   public BridgeDecl maxAttempts(int v) {
091      if (v < 1)
092         throw e51(id, "maxAttempts must be >= 1; got " + v);
093      maxAttempts = v;
094      return this;
095   }
096
097   /** @return The id. */
098   public String id() {
099      return id;
100   }
101
102   /** @return The {@code $defs/bridgeDecl} entry, with {@code session} unresolved. */
103   public JsonMap toMap() {
104      if (downstream.isEmpty() && upstream.isEmpty())
105         throw e51(id, "carries no topics; add downstream(...) or upstream(...)");
106      for (var t : upstream)
107         if (downstream.contains(t))
108            throw e51(id, "topic '" + t + "' is both downstream and upstream");
109      var m = new JsonMap();
110      m.put("id", id);
111      m.put("transport", transport);
112      m.put("session", session);
113      m.put("downstream", List.copyOf(downstream));
114      if (! upstream.isEmpty())
115         m.put("upstream", List.copyOf(upstream));
116      if (maxAttempts != null)
117         m.put("maxAttempts", maxAttempts);
118      return m;
119   }
120
121   static IllegalArgumentException e51(String id, String detail) {
122      return Topics.iae("bridge '%s': %s", id, detail);
123   }
124}