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.console.TopicDecl.*; 026 027import freemarker.core.*; 028import freemarker.template.*; 029 030/** 031 * {@code <@topic name="..." retain=true|false publisher="script|server|ribbon"/>}: declares a page-level custom topic 032 * in the contract's {@code topics} list. The FTL twin of {@code PageSpec.topic(TopicDecl)}; the two union. 033 * 034 * <h5 class='section'>Example:</h5> 035 * <p class='bcode'> 036 * <@page> 037 * <@topic name="app.region-picked" retain=true publisher="script"/> 038 * <@card type="html" id="region" subscribes="app.region-picked">...</@card> 039 * </@page> 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 TopicDirectiveModel implements TemplateDirectiveModel { 048 049 /** The shared-variable name this directive registers under. */ 050 public static final String NAME = "topic"; 051 052 static final Set<String> ATTRS = Set.of("name", "retain", "publisher"); 053 054 TopicDirectiveModel() {} 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("<@topic> must be inside <@console> or <@page>"); 066 FtlAttrLists.rejectUnknown(p, NAME, ATTRS); 067 var name = FtlAttrLists.scalar(p, "name"); 068 if (! p.containsKey("retain")) 069 throw FtlAttrLists.reject(f("<@topic name='%s'> needs retain=true or retain=false", name)); 070 var retain = FtlAttrLists.strictBoolean(p, NAME, "retain", false); 071 var publisher = publisher(name, FtlAttrLists.scalar(p, "publisher")); 072 try { 073 var d = TopicDecl.of(name).retain(retain); 074 if (publisher != null) 075 d.publisher(publisher); 076 cap.addTopic(d.toMap(), "<@topic>"); 077 } catch (IllegalArgumentException e) { 078 throw FtlAttrLists.reject(e.getMessage()); 079 } 080 } 081 082 private static Publisher publisher(String name, String v) throws TemplateModelException { 083 if (v.isEmpty()) 084 return null; // TopicDecl.toMap() reports the missing publisher with the same text as Java. 085 return switch (v) { 086 case "script" -> Publisher.SCRIPT; 087 case "server" -> Publisher.SERVER; 088 case "ribbon" -> Publisher.RIBBON; 089 default -> throw FtlAttrLists.reject(f("<@topic name='%s'> publisher='%s' must be script, server or ribbon", name, v)); 090 }; 091 } 092}