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(...)} &mdash; <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>&lt;context-param&gt;</xt>
053 *       <xt>&lt;param-name&gt;</xt>juneau.rest.auto-register<xt>&lt;/param-name&gt;</xt>
054 *       <xt>&lt;param-value&gt;</xt>true<xt>&lt;/param-value&gt;</xt>
055 *    <xt>&lt;/context-param&gt;</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}