| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.startup; |
| |
| import java.io.BufferedReader; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| |
| import jakarta.servlet.ServletContext; |
| |
| import org.apache.catalina.Context; |
| import org.apache.tomcat.util.scan.JarFactory; |
| |
| /** |
| * A variation of Java's JAR ServiceLoader that respects exclusion rules for web applications. |
| * <p> |
| * Primarily intended for use loading ServletContainerInitializers as defined by Servlet 8.2.4. This implementation does |
| * not attempt lazy loading as the container is required to introspect all implementations discovered. |
| * <p> |
| * If the ServletContext defines ORDERED_LIBS, then only JARs in WEB-INF/lib that are named in that set will be included |
| * in the search for provider configuration files; if ORDERED_LIBS is not defined then all JARs will be searched for |
| * provider configuration files. Providers defined by resources in the parent ClassLoader will always be returned. |
| * <p> |
| * Provider classes will be loaded using the context's ClassLoader. |
| * |
| * @param <T> The type of service to load |
| * |
| * @see jakarta.servlet.ServletContainerInitializer |
| * @see java.util.ServiceLoader |
| */ |
| public class WebappServiceLoader<T> { |
| private static final String CLASSES = "/WEB-INF/classes/"; |
| private static final String LIB = "/WEB-INF/lib/"; |
| private static final String SERVICES = "META-INF/services/"; |
| |
| private final Context context; |
| private final ServletContext servletContext; |
| private final Pattern containerSciFilterPattern; |
| |
| |
| /** |
| * Construct a loader to load services from a ServletContext. |
| * |
| * @param context the context to use |
| */ |
| public WebappServiceLoader(Context context) { |
| this.context = context; |
| this.servletContext = context.getServletContext(); |
| String containerSciFilter = context.getContainerSciFilter(); |
| if (containerSciFilter != null && !containerSciFilter.isEmpty()) { |
| containerSciFilterPattern = Pattern.compile(containerSciFilter); |
| } else { |
| containerSciFilterPattern = null; |
| } |
| } |
| |
| |
| /** |
| * Load the providers for a service type. Container defined services will be loaded before application defined |
| * services in case the application depends on a Container provided service. Note that services are always loaded |
| * via the Context (web application) class loader so it is possible for an application to provide an alternative |
| * implementation of what would normally be a Container provided service. |
| * |
| * @param serviceType the type of service to load |
| * |
| * @return an unmodifiable collection of service providers |
| * |
| * @throws IOException if there was a problem loading any service |
| */ |
| public List<T> load(Class<T> serviceType) throws IOException { |
| String configFile = SERVICES + serviceType.getName(); |
| |
| // Obtain the Container provided service configuration files. |
| ClassLoader loader = context.getParentClassLoader(); |
| Enumeration<URL> containerResources; |
| if (loader == null) { |
| containerResources = ClassLoader.getSystemResources(configFile); |
| } else { |
| containerResources = loader.getResources(configFile); |
| } |
| |
| // Extract the Container provided service class names. Each |
| // configuration file may list more than one service class name. This |
| // uses a LinkedHashSet so if a service class name appears more than |
| // once in the configuration files, only the first one found is used. |
| LinkedHashSet<String> containerServiceClassNames = new LinkedHashSet<>(); |
| Set<URL> containerServiceConfigFiles = new HashSet<>(); |
| while (containerResources.hasMoreElements()) { |
| URL containerServiceConfigFile = containerResources.nextElement(); |
| containerServiceConfigFiles.add(containerServiceConfigFile); |
| parseConfigFile(containerServiceClassNames, containerServiceConfigFile); |
| } |
| |
| // Filter the discovered container SCIs if required |
| if (containerSciFilterPattern != null) { |
| containerServiceClassNames.removeIf(s -> containerSciFilterPattern.matcher(s).find()); |
| } |
| |
| // Obtaining the application provided configuration files is a little |
| // more difficult for two reasons: |
| // - The web application may employ a custom class loader. Ideally, we |
| // would use ClassLoader.findResources() but that method is protected. |
| // We could force custom class loaders to override that method and |
| // make it public but that would be a new requirement and break |
| // backwards compatibility for what is an often customised component. |
| // - If the application web.xml file has defined an order for fragments |
| // then only those JAR files represented by fragments in that order |
| // (and arguably WEB-INF/classes) should be scanned for services. |
| LinkedHashSet<String> applicationServiceClassNames = new LinkedHashSet<>(); |
| |
| // Check to see if the ServletContext has ORDERED_LIBS defined |
| @SuppressWarnings("unchecked") |
| List<String> orderedLibs = (List<String>) servletContext.getAttribute(ServletContext.ORDERED_LIBS); |
| |
| // Obtain the application provided service configuration files |
| if (orderedLibs == null) { |
| // Because a custom class loader may be being used, we have to use |
| // getResources() which will return application and Container files. |
| Enumeration<URL> allResources = servletContext.getClassLoader().getResources(configFile); |
| while (allResources.hasMoreElements()) { |
| URL serviceConfigFile = allResources.nextElement(); |
| // Only process the service configuration file if it is not a |
| // Container level file that has already been processed |
| if (!containerServiceConfigFiles.contains(serviceConfigFile)) { |
| parseConfigFile(applicationServiceClassNames, serviceConfigFile); |
| } |
| } |
| } else { |
| // Ordered libs so only use services defined in those libs and any |
| // in WEB-INF/classes |
| URL unpacked = servletContext.getResource(CLASSES + configFile); |
| if (unpacked != null) { |
| parseConfigFile(applicationServiceClassNames, unpacked); |
| } |
| |
| for (String lib : orderedLibs) { |
| URL jarUrl = servletContext.getResource(LIB + lib); |
| if (jarUrl == null) { |
| // should not happen, just ignore |
| continue; |
| } |
| |
| String base = jarUrl.toExternalForm(); |
| URL url; |
| if (base.endsWith("/")) { |
| URI uri; |
| try { |
| uri = new URI(base + configFile); |
| } catch (URISyntaxException e) { |
| // Not ideal but consistent with public API |
| throw new IOException(e); |
| } |
| url = uri.toURL(); |
| } else { |
| url = JarFactory.getJarEntryURL(jarUrl, configFile); |
| } |
| try { |
| parseConfigFile(applicationServiceClassNames, url); |
| } catch (FileNotFoundException e) { |
| // no provider file found, this is OK |
| } |
| } |
| } |
| |
| // Add the application services after the container services to ensure |
| // that the container services are loaded first |
| containerServiceClassNames.addAll(applicationServiceClassNames); |
| |
| // Short-cut if no services have been found |
| if (containerServiceClassNames.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| // Load the discovered services |
| return loadServices(serviceType, containerServiceClassNames); |
| } |
| |
| |
| void parseConfigFile(LinkedHashSet<String> servicesFound, URL url) throws IOException { |
| try (InputStream is = url.openStream(); |
| InputStreamReader in = new InputStreamReader(is, StandardCharsets.UTF_8); |
| BufferedReader reader = new BufferedReader(in)) { |
| String line; |
| while ((line = reader.readLine()) != null) { |
| int i = line.indexOf('#'); |
| if (i >= 0) { |
| line = line.substring(0, i); |
| } |
| line = line.trim(); |
| if (line.isEmpty()) { |
| continue; |
| } |
| servicesFound.add(line); |
| } |
| } |
| } |
| |
| |
| List<T> loadServices(Class<T> serviceType, LinkedHashSet<String> servicesFound) throws IOException { |
| ClassLoader loader = servletContext.getClassLoader(); |
| List<T> services = new ArrayList<>(servicesFound.size()); |
| for (String serviceClass : servicesFound) { |
| try { |
| Class<?> clazz = Class.forName(serviceClass, true, loader); |
| services.add(serviceType.cast(clazz.getConstructor().newInstance())); |
| } catch (ReflectiveOperationException | ClassCastException e) { |
| throw new IOException(e); |
| } |
| } |
| return Collections.unmodifiableList(services); |
| } |
| } |