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 java.util.*;
020
021import org.apache.juneau.commons.inject.*;
022import org.apache.juneau.rest.server.*;
023import org.apache.juneau.rest.server.servlet.*;
024import org.springframework.boot.autoconfigure.*;
025import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
026import org.springframework.boot.web.servlet.*;
027import org.springframework.context.*;
028import org.springframework.context.annotation.Bean;
029
030import jakarta.servlet.*;
031
032/**
033 * Opt-in Spring Boot auto-configuration that mounts every {@link RestServlet} bean at its
034 * self-declared top-level path without an explicit {@link ServletRegistrationBean} per servlet.
035 *
036 * <h5 class='section'>Activation:</h5>
037 *
038 * <p>
039 * Gated behind the property {@code juneau.rest.auto-register=true} (no default &mdash;
040 * {@code matchIfMissing=false}); when the property is absent or any value other than {@code true},
041 * this auto-configuration contributes nothing and servlet registration stays fully manual. Set it
042 * in {@code application.properties} / {@code application.yaml}:
043 *
044 * <p class='bcode'>
045 *    juneau.rest.auto-register=true
046 * </p>
047 *
048 * <h5 class='section'>Behavior:</h5>
049 *
050 * <p>
051 * When active, a single {@link ServletContextInitializer} bean walks all {@link RestServlet} beans
052 * in the application context and, for each one:
053 *
054 * <ol class='spaced-list'>
055 *    <li>Skips it if its instance is already wired into a manual {@link ServletRegistrationBean}
056 *       (no double-mounts &mdash; explicit registrations always win).
057 *    <li>Resolves its mount paths via
058 *       {@link RestContext#resolveTopLevelPaths(Class, Object, BeanStore) RestContext.resolveTopLevelPaths(...)}
059 *       (reading the top-level {@code paths()} / {@code getPaths()} chain &mdash; <b>not</b>
060 *       {@code path()}), using the application's registered {@link BeanStore} for SVL resolution.
061 *    <li>Registers the servlet at each resolved path. A servlet that resolves to no top-level paths
062 *       is skipped (it is presumably composed as a child or mounted some other way).
063 * </ol>
064 *
065 * <h5 class='section'>See Also:</h5><ul>
066 *    <li class='jc'>{@link JuneauServletRegistrations}
067 *    <li class='jm'>{@link RestContext#resolveTopLevelPaths(Class, Object, BeanStore)}
068 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestServerSpringboot">juneau-rest-server-springboot</a>
069 * </ul>
070 *
071 * @since 10.0.0
072 */
073@AutoConfiguration
074@ConditionalOnProperty(name="juneau.rest.auto-register", havingValue="true", matchIfMissing=false)
075public class JuneauRestAutoConfiguration {
076
077   /**
078    * Contributes the auto-registration hook.
079    *
080    * @param appContext The Spring application context (source of {@link RestServlet} beans,
081    *    existing {@link ServletRegistrationBean} beans, and the {@link BeanStore}).
082    * @return A {@link ServletContextInitializer} that mounts un-registered Juneau servlets.
083    */
084   @Bean
085   public ServletContextInitializer juneauRestAutoRegistrar(ApplicationContext appContext) {
086      return servletContext -> registerAll(appContext, servletContext);
087   }
088
089   private static void registerAll(ApplicationContext appContext, ServletContext servletContext) {
090      var servlets = appContext.getBeansOfType(RestServlet.class);
091      if (servlets.isEmpty())
092         return;
093
094      // Identity set of servlet instances already wired into a manual ServletRegistrationBean — these
095      // must never be auto-registered a second time (explicit registration wins).
096      var manual = Collections.newSetFromMap(new IdentityHashMap<Servlet,Boolean>());
097      for (var bean : appContext.getBeansOfType(ServletRegistrationBean.class).values()) {
098         var s = bean.getServlet();
099         if (s != null)
100            manual.add(s);
101      }
102
103      var store = appContext.getBeanProvider(BeanStore.class).getIfAvailable();
104
105      for (var e : servlets.entrySet())
106         register(servletContext, e.getKey(), e.getValue(), manual, store);
107   }
108
109   private static void register(ServletContext servletContext, String name, RestServlet servlet, Set<Servlet> manual, BeanStore store) {
110      if (manual.contains(servlet))
111         return;
112      var resolved = RestContext.resolveTopLevelPaths(servlet.getClass(), servlet, store);
113      if (resolved.length == 0)
114         return;
115      var reg = servletContext.addServlet(name, servlet);
116      if (reg == null)
117         return; // A servlet with this name is already registered (container-level dedup).
118      for (var path : resolved)
119         reg.addMapping(toUrlMapping(path));
120   }
121
122   private static String toUrlMapping(String rawPath) {
123      var p = rawPath;
124      while (p.endsWith("/") && p.length() > 1)
125         p = p.substring(0, p.length() - 1);
126      if (! p.startsWith("/"))
127         p = "/" + p;
128      return p;
129   }
130}