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 *    &lt;@topic name="ops.jobs" retain=true publisher="server"/&gt;
039 *    &lt;@bridge id="ops" transport="sse" session="servlet:/juneau-bus/session" downstream="ops.jobs"/&gt;
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}