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&lt;?&gt; 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 * &mdash; the same runtime-override resolution chain the Jetty microservice auto-discovery loop and
047 * the Spring Boot auto-configuration use &mdash; 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) &mdash; 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}