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.rest.server.springboot; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import org.apache.juneau.commons.inject.*; 022import org.apache.juneau.rest.server.*; 023import org.springframework.boot.web.servlet.*; 024 025import jakarta.servlet.*; 026 027/** 028 * Always-available factory helper that builds a Spring Boot {@link ServletRegistrationBean} whose 029 * URL mappings are derived from a Juneau servlet's self-declared top-level paths. 030 * 031 * <p> 032 * Use this from an explicit {@code @Bean} method when you want the mount path to come from the 033 * servlet's own {@link Rest#paths() @Rest(paths)} declaration 034 * (or its {@code getPaths()} override) rather than a hard-coded string in your configuration class: 035 * 036 * <p class='bjava'> 037 * <ja>@Bean</ja> 038 * <jk>public</jk> ServletRegistrationBean<?> jspServlet(BeanStore <jv>beanStore</jv>) { 039 * <jk>return</jk> JuneauServletRegistrations.<jsm>forServlet</jsm>(<jk>new</jk> JspServlet(), <jv>beanStore</jv>); 040 * } 041 * </p> 042 * 043 * <p> 044 * The mapping is resolved via 045 * {@link RestContext#resolveTopLevelPaths(Class, Object, BeanStore) RestContext.resolveTopLevelPaths(...)} 046 * — the same runtime-override resolution chain the Jetty microservice auto-discovery loop and 047 * the Spring Boot auto-configuration use — so behavior is identical across runtimes. No path 048 * string is hard-coded here; a servlet that resolves to no top-level paths is rejected with an 049 * {@link IllegalArgumentException} (declare {@code @Rest(paths=...)} or override {@code getPaths()}). 050 * 051 * <h5 class='section'>See Also:</h5><ul> 052 * <li class='jc'>{@link JuneauRestAutoConfiguration} 053 * <li class='jm'>{@link RestContext#resolveTopLevelPaths(Class, Object, BeanStore)} 054 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestServerSpringboot">juneau-rest-server-springboot</a> 055 * </ul> 056 * 057 * @since 10.0.0 058 */ 059public class JuneauServletRegistrations { 060 061 private JuneauServletRegistrations() {} 062 063 /** 064 * Builds a {@link ServletRegistrationBean} for the supplied Juneau servlet, mapping it at the 065 * top-level paths it self-declares. 066 * 067 * @param <T> The servlet type. 068 * @param servlet The Juneau REST servlet to register. Must not be {@code null} and must carry a 069 * {@link Rest @Rest} annotation that resolves to at least one 070 * top-level path (via {@code paths()} or a {@code getPaths()} override). 071 * @param beanStore The bean store used for SVL resolution of {@code @Rest(paths)} elements 072 * (e.g. {@code $C{key}} / {@code ${env}} templates). May be {@code null} (SVL is then skipped; 073 * literal paths pass through unchanged). 074 * @return A registration bean mapping the servlet at its self-declared paths. 075 * @throws IllegalArgumentException If {@code servlet} is {@code null} or resolves to no 076 * top-level paths. 077 */ 078 public static <T extends Servlet> ServletRegistrationBean<T> forServlet(T servlet, BeanStore beanStore) { 079 if (servlet == null) 080 throw iaex("Argument 'servlet' must not be null"); 081 var resolved = RestContext.resolveTopLevelPaths(servlet.getClass(), servlet, beanStore); 082 if (resolved.length == 0) 083 throw iaex("Servlet %s resolves to no top-level paths; declare @Rest(paths=...) or override getPaths().", cn(servlet)); 084 var mappings = new String[resolved.length]; 085 for (var i = 0; i < resolved.length; i++) 086 mappings[i] = toUrlMapping(resolved[i]); 087 var bean = new ServletRegistrationBean<>(servlet, mappings); 088 bean.setName(cn(servlet)); 089 return bean; 090 } 091 092 /** 093 * Normalizes a resolved top-level path into a servlet URL mapping. 094 * 095 * <p> 096 * Mirrors the microservice {@code JettyServerComponent.normalizeExactPathSpec(...)} contract: 097 * trims trailing slashes, ensures a leading slash, and leaves any {@code /*} suffix intact (so a 098 * prefix mount like {@code /jsp/*} stays a prefix mapping while an exact mount like 099 * {@code /healthz} stays exact) — cross-runtime parity with the Jetty mount path. 100 */ 101 private static String toUrlMapping(String rawPath) { 102 var p = rawPath; 103 while (p.endsWith("/") && p.length() > 1) 104 p = p.substring(0, p.length() - 1); 105 if (! p.startsWith("/")) 106 p = "/" + p; 107 return p; 108 } 109}