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.freemarker; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.console.*; 025import org.apache.juneau.rest.server.view.freemarker.*; 026 027import freemarker.core.*; 028import freemarker.template.*; 029import freemarker.template.utility.*; 030 031/** 032 * {@code <@bridge id transport session downstream upstream? maxAttempts?/>}: declares a server bridge in the 033 * contract's {@code bridges} list. The FTL twin of {@code PageSpec.bridge(BridgeDecl)}. {@code session} may be a 034 * {@code servlet:} / {@code context:} URI; it is resolved against the current request. 035 * 036 * <h5 class='section'>Example:</h5> 037 * <p class='bcode'> 038 * <@topic name="ops.jobs" retain=true publisher="server"/> 039 * <@bridge id="ops" transport="sse" session="servlet:/juneau-bus/session" downstream="ops.jobs"/> 040 * </p> 041 * 042 * @since 10.0.0 043 */ 044@SuppressWarnings({ 045 "java:S1192" // Repeated literals are contract wire keys; named constants would obscure the contract shape. 046}) 047public final class BridgeDirectiveModel implements TemplateDirectiveModel { 048 049 /** The shared-variable name this directive registers under. */ 050 public static final String NAME = "bridge"; 051 052 static final Set<String> ATTRS = Set.of("id", "transport", "session", "downstream", "upstream", "maxAttempts"); 053 054 BridgeDirectiveModel() {} 055 056 @Override 057 @SuppressWarnings({ 058 "unchecked" // FreeMarker's raw params Map is String-keyed by contract. 059 }) 060 public void execute(Environment env, @SuppressWarnings("rawtypes") Map params, TemplateModel[] loopVars, 061 TemplateDirectiveBody body) throws TemplateException, IOException { 062 Map<String, TemplateModel> p = params; 063 var cap = PageCapture.get(env); 064 if (cap == null || ! (cap.consoleOpen || cap.pageOpen)) 065 throw FtlAttrLists.reject("<@bridge> must be inside <@console> or <@page>"); 066 FtlAttrLists.rejectUnknown(p, NAME, ATTRS); 067 var id = FtlAttrLists.scalar(p, "id"); 068 var transport = FtlAttrLists.scalar(p, "transport"); 069 var session = FtlAttrLists.scalar(p, "session"); 070 try { 071 var b = switch (transport) { 072 case "sse" -> BridgeDecl.sse(id, session); 073 case "websocket" -> BridgeDecl.websocket(id, session); 074 default -> throw iaex("bridge '%s': transport '%s' must be sse or websocket", id, transport); 075 }; 076 b.downstream(FtlAttrLists.list(p, NAME, "downstream").toArray(String[]::new)); 077 var up = FtlAttrLists.list(p, NAME, "upstream"); 078 if (! up.isEmpty()) 079 b.upstream(up.toArray(String[]::new)); 080 if (p.containsKey("maxAttempts")) 081 b.maxAttempts(maxAttempts(id, DeepUnwrap.unwrap(p.get("maxAttempts")))); 082 cap.addBridge(PageSpec.resolvedBridge(b, FreemarkerRenderScope.request())); 083 } catch (IllegalArgumentException e) { 084 throw FtlAttrLists.reject(e.getMessage()); 085 } 086 } 087 088 private static int maxAttempts(String id, Object v) { 089 if (v instanceof Number n && n.doubleValue() == n.intValue()) 090 return n.intValue(); 091 try { 092 return Integer.parseInt(String.valueOf(v).trim()); 093 } catch (NumberFormatException e) { 094 throw iaex("bridge '%s': maxAttempts must be an integer; got '%s'", id, v); 095 } 096 } 097}