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.servlet; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.lang.reflect.*; 022import java.util.*; 023 024import org.apache.juneau.rest.server.*; 025 026import jakarta.servlet.*; 027import jakarta.servlet.annotation.*; 028 029/** 030 * Plain Servlet-spec {@link ServletContainerInitializer} that auto-mounts {@link RestServlet} 031 * subclasses at their self-declared top-level paths in non-Spring (e.g. {@code web.xml} / external 032 * WAR) deployments. 033 * 034 * <p> 035 * Discovered by the container via {@code META-INF/services/jakarta.servlet.ServletContainerInitializer} 036 * and (through {@link HandlesTypes @HandlesTypes(RestServlet.class)}) handed every concrete 037 * {@link RestServlet} on the webapp classpath. It mirrors the Jetty-microservice and Spring Boot 038 * auto-registration mechanisms: the mount paths are read from the top-level chain 039 * ({@link Rest#paths() @Rest(paths)} / {@code getPaths()}) via 040 * {@link RestContext#resolveTopLevelPaths(Class, Object, org.apache.juneau.commons.inject.BeanStore) 041 * RestContext.resolveTopLevelPaths(...)} — <b>not</b> {@code path()} (child-composition). 042 * 043 * <h5 class='section'>Activation:</h5> 044 * 045 * <p> 046 * Opt-in via the context init parameter {@code juneau.rest.auto-register=true} (parity with the 047 * Spring Boot {@code juneau.rest.auto-register} property). When unset or not {@code true}, the 048 * initializer does nothing, so it never collides with servlets the deployer declared manually in 049 * {@code web.xml}. Declare it in {@code web.xml}: 050 * 051 * <p class='bxml'> 052 * <xt><context-param></xt> 053 * <xt><param-name></xt>juneau.rest.auto-register<xt></param-name></xt> 054 * <xt><param-value></xt>true<xt></param-value></xt> 055 * <xt></context-param></xt> 056 * </p> 057 * 058 * <h5 class='section'>Behavior:</h5> 059 * 060 * <p> 061 * For each handled class that is concrete, carries a {@link Rest @Rest} annotation, is no-arg 062 * instantiable, and resolves to at least one top-level path, an instance is created and added to the 063 * servlet context at each resolved path. Classes that resolve to no top-level paths (pure mixins / 064 * child-composed resources) or that cannot be instantiated with a no-arg constructor are skipped. 065 * 066 * <h5 class='section'>See Also:</h5><ul> 067 * <li class='jm'>{@link RestContext#resolveTopLevelPaths(Class, Object, org.apache.juneau.commons.inject.BeanStore)} 068 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 069 * </ul> 070 * 071 * @since 10.0.0 072 */ 073@HandlesTypes(RestServlet.class) 074public class JuneauRestServletContainerInitializer implements ServletContainerInitializer { 075 076 /** Context init parameter that opts a webapp into auto-registration. */ 077 public static final String AUTO_REGISTER_PARAM = "juneau.rest.auto-register"; 078 079 @Override /* ServletContainerInitializer */ 080 public void onStartup(Set<Class<?>> classes, ServletContext ctx) throws ServletException { 081 if (neqic("true", ctx.getInitParameter(AUTO_REGISTER_PARAM))) 082 return; 083 if (classes == null) 084 return; 085 086 for (var c : classes) 087 tryMount(c, ctx); 088 } 089 090 private static void tryMount(Class<?> c, ServletContext ctx) { 091 if (! isMountable(c)) 092 return; 093 var servlet = instantiate(c); 094 if (servlet == null) 095 return; 096 var resolved = RestContext.resolveTopLevelPaths(c, servlet, null); 097 if (resolved.length == 0) 098 return; 099 var reg = ctx.addServlet(c.getName(), servlet); 100 if (reg == null) 101 return; // A servlet with this name is already registered (e.g. via web.xml). 102 for (var path : resolved) 103 reg.addMapping(toUrlMapping(path)); 104 } 105 106 private static boolean isMountable(Class<?> c) { 107 if (c == null || c.isInterface() || Modifier.isAbstract(c.getModifiers())) 108 return false; 109 return c.isAnnotationPresent(Rest.class); 110 } 111 112 private static RestServlet instantiate(Class<?> c) { 113 try { 114 return (RestServlet) c.getDeclaredConstructor().newInstance(); 115 } catch (ReflectiveOperationException | RuntimeException e) { 116 // Best-effort: a servlet that needs constructor-injected collaborators can't be 117 // auto-mounted by the plain Servlet-spec path and is simply skipped here. 118 return null; 119 } 120 } 121 122 private static String toUrlMapping(String rawPath) { 123 var p = rawPath == null ? "" : rawPath; 124 while (p.endsWith("/") && p.length() > 1) 125 p = p.substring(0, p.length() - 1); 126 if (p.isEmpty()) 127 return "/"; 128 if (! p.startsWith("/")) 129 p = "/" + p; 130 return p; 131 } 132}