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}