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 — 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 — 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 — <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}