blob: b58c8d9252457919cb4d144bdab0d48972176c1c [file] [log] [blame]
/*
* 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.solr.core;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableSet;
import org.apache.lucene.analysis.WordlistLoader;
import org.apache.lucene.analysis.util.*;
import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.DocValuesFormat;
import org.apache.lucene.codecs.PostingsFormat;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.SolrClassLoader;
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.handler.component.ShardHandlerFactory;
import org.apache.solr.logging.DeprecationLog;
import org.apache.solr.pkg.PackageListeningClassLoader;
import org.apache.solr.pkg.PackageLoader;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.rest.RestManager;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.ManagedIndexSchemaFactory;
import org.apache.solr.schema.SimilarityFactory;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.update.processor.UpdateRequestProcessorFactory;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @since solr 1.3
*/
public class SolrResourceLoader implements ResourceLoader, Closeable, SolrClassLoader, SolrCoreAware {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String base = "org.apache.solr";
private static final String[] packages = {
"", "analysis.", "schema.", "handler.", "handler.tagger.", "search.", "update.", "core.", "response.", "request.",
"update.processor.", "util.", "spelling.", "handler.component.", "handler.dataimport.",
"spelling.suggest.", "spelling.suggest.fst.", "rest.schema.analysis.", "security.", "handler.admin.",
"cloud.autoscaling."
};
private static final Charset UTF_8 = StandardCharsets.UTF_8;
private String name = "";
protected URLClassLoader classLoader;
private final Path instanceDir;
private String dataDir; // gone in 9.0
private String coreName;
private UUID coreId;
private SolrConfig config;
private CoreContainer coreContainer;
private PackageListeningClassLoader schemaLoader ;
private PackageListeningClassLoader coreReloadingClassLoader ;
private final List<SolrCoreAware> waitingForCore = Collections.synchronizedList(new ArrayList<SolrCoreAware>());
private final List<SolrInfoBean> infoMBeans = Collections.synchronizedList(new ArrayList<SolrInfoBean>());
private final List<ResourceLoaderAware> waitingForResources = Collections.synchronizedList(new ArrayList<ResourceLoaderAware>());
private final Properties coreProperties; // gone in 9.0
private volatile boolean live;
// Provide a registry so that managed resources can register themselves while the XML configuration
// documents are being parsed ... after all are registered, they are asked by the RestManager to
// initialize themselves. This two-step process is required because not all resources are available
// (such as the SolrZkClient) when XML docs are being parsed.
private RestManager.Registry managedResourceRegistry;
/** @see #reloadLuceneSPI() */
private boolean needToReloadLuceneSPI = false; // requires synchronization
public synchronized RestManager.Registry getManagedResourceRegistry() {
if (managedResourceRegistry == null) {
managedResourceRegistry = new RestManager.Registry();
}
return managedResourceRegistry;
}
public SolrClassLoader getSchemaLoader() {
if (schemaLoader == null) {
schemaLoader = createSchemaLoader();
}
return schemaLoader;
}
/**
* @deprecated Use <code>new SolrResourceLoader(Path)</code>
* @see CoreContainer#getSolrHome
*/
@Deprecated
public SolrResourceLoader() {
this(SolrPaths.locateSolrHome(), null);
}
/**
* Creates a loader.
* Note: we do NOT call {@link #reloadLuceneSPI()}.
* (Behavior when <code>instanceDir</code> is <code>null</code> is un-specified, in future versions this will fail due to NPE)
*/
public SolrResourceLoader(String name, List<Path> classpath, Path instanceDir, ClassLoader parent) {
this(instanceDir, parent);
this.name = name;
final List<URL> libUrls = new ArrayList<>(classpath.size());
try {
for (Path path : classpath) {
libUrls.add(path.toUri().normalize().toURL());
}
} catch (MalformedURLException e) { // impossible?
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
}
addToClassLoader(libUrls);
}
/**
* Creates a loader.
* (Behavior when <code>instanceDir</code> is <code>null</code> is un-specified, in future versions this will fail due to NPE)
*/
public SolrResourceLoader(Path instanceDir) {
this(instanceDir, null);
}
public SolrResourceLoader(Path instanceDir, ClassLoader parent) {
this(instanceDir, parent, null);
}
/**
* @param instanceDir - base directory for this resource loader, if null locateSolrHome() will be used. (in future versions this will fail due to NPE)
* @see SolrPaths#locateSolrHome()
*/
@Deprecated // the Properties arg in particular is what is deprecated
public SolrResourceLoader(Path instanceDir, ClassLoader parent, Properties properties) {
this.coreProperties = properties;
if (instanceDir == null) {
this.instanceDir = SolrPaths.locateSolrHome().toAbsolutePath().normalize();
log.warn("SolrResourceLoader created with null instanceDir. This will not be supported in Solr 9.0");
log.debug("new SolrResourceLoader for deduced Solr Home: '{}'", this.instanceDir);
} else{
this.instanceDir = instanceDir.toAbsolutePath().normalize();
log.debug("new SolrResourceLoader for directory: '{}'", this.instanceDir);
}
if (parent == null) {
parent = getClass().getClassLoader();
}
this.classLoader = URLClassLoader.newInstance(new URL[0], parent);
}
/**
* Adds URLs to the ResourceLoader's internal classloader. This method <b>MUST</b>
* only be called prior to using this ResourceLoader to get any resources, otherwise
* its behavior will be non-deterministic. You also have to {link @reloadLuceneSPI}
* before using this ResourceLoader.
*
* @param urls the URLs of files to add
*/
synchronized void addToClassLoader(List<URL> urls) {
URLClassLoader newLoader = addURLsToClassLoader(classLoader, urls);
if (newLoader == classLoader) {
return; // short-circuit
}
this.classLoader = newLoader;
this.needToReloadLuceneSPI = true;
if (log.isInfoEnabled()) {
log.info("Added {} libs to classloader, from paths: {}",
urls.size(), urls.stream()
.map(u -> u.getPath().substring(0, u.getPath().lastIndexOf("/")))
.sorted()
.distinct()
.collect(Collectors.toList()));
}
}
/**
* Reloads all Lucene SPI implementations using the new classloader.
* This method must be called after {@link #addToClassLoader(List)}
* and before using this ResourceLoader.
*/
synchronized void reloadLuceneSPI() {
// TODO improve to use a static Set<URL> to check when we need to
if (!needToReloadLuceneSPI) {
return;
}
needToReloadLuceneSPI = false; // reset
log.debug("Reloading Lucene SPI");
// Codecs:
PostingsFormat.reloadPostingsFormats(this.classLoader);
DocValuesFormat.reloadDocValuesFormats(this.classLoader);
Codec.reloadCodecs(this.classLoader);
// Analysis:
CharFilterFactory.reloadCharFilters(this.classLoader);
TokenFilterFactory.reloadTokenFilters(this.classLoader);
TokenizerFactory.reloadTokenizers(this.classLoader);
}
private static URLClassLoader addURLsToClassLoader(final URLClassLoader oldLoader, List<URL> urls) {
if (urls.size() == 0) {
return oldLoader;
}
List<URL> allURLs = new ArrayList<>();
allURLs.addAll(Arrays.asList(oldLoader.getURLs()));
allURLs.addAll(urls);
for (URL url : urls) {
if (log.isDebugEnabled()) {
log.debug("Adding '{}' to classloader", url);
}
}
ClassLoader oldParent = oldLoader.getParent();
IOUtils.closeWhileHandlingException(oldLoader);
return URLClassLoader.newInstance(allURLs.toArray(new URL[allURLs.size()]), oldParent);
}
/**
* Utility method to get the URLs of all paths under a given directory that match a filter
* @param libDir the root directory
* @param filter the filter
* @return all matching URLs
* @throws IOException on error
*/
public static List<URL> getURLs(Path libDir, DirectoryStream.Filter<Path> filter) throws IOException {
List<URL> urls = new ArrayList<>();
try (DirectoryStream<Path> directory = Files.newDirectoryStream(libDir, filter)) {
for (Path element : directory) {
urls.add(element.toUri().normalize().toURL());
}
}
return urls;
}
/**
* Utility method to get the URLs of all paths under a given directory
* @param libDir the root directory
* @return all subdirectories as URLs
* @throws IOException on error
*/
public static List<URL> getURLs(Path libDir) throws IOException {
return getURLs(libDir, new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return true;
}
});
}
/**
* Utility method to get the URLs of all paths under a given directory that match a regex
* @param libDir the root directory
* @param regex the regex as a String
* @return all matching URLs
* @throws IOException on error
*/
public static List<URL> getFilteredURLs(Path libDir, String regex) throws IOException {
final PathMatcher matcher = libDir.getFileSystem().getPathMatcher("regex:" + regex);
return getURLs(libDir, new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return matcher.matches(entry.getFileName());
}
});
}
/** Ensures a directory name always ends with a '/'. */
@Deprecated
public static String normalizeDir(String path) {
return ( path != null && (!(path.endsWith("/") || path.endsWith("\\"))) )? path + File.separator : path;
}
@Deprecated
public String[] listConfigDir() {
File configdir = new File(getConfigDir());
if( configdir.exists() && configdir.isDirectory() ) {
return configdir.list();
} else {
return new String[0];
}
}
public String getConfigDir() {
return instanceDir.resolve("conf").toString();
}
@Deprecated
public String getDataDir() {
return dataDir;
}
@Deprecated
public Properties getCoreProperties() {
return coreProperties;
}
/**
* EXPERT
* <p>
* The underlying class loader. Most applications will not need to use this.
* @return The {@link ClassLoader}
*/
public ClassLoader getClassLoader() {
return classLoader;
}
/** Opens a schema resource by its name.
* Override this method to customize loading schema resources.
*@return the stream for the named schema
*/
@Deprecated
public InputStream openSchema(String name) throws IOException {
return openResource(name);
}
/** Opens a config resource by its name.
* Override this method to customize loading config resources.
*@return the stream for the named configuration
*/
@Deprecated
public InputStream openConfig(String name) throws IOException {
return openResource(name);
}
private Path checkPathIsSafe(Path pathToCheck) throws IOException {
if (Boolean.getBoolean("solr.allow.unsafe.resourceloading"))
return pathToCheck;
pathToCheck = pathToCheck.normalize();
if (pathToCheck.startsWith(instanceDir))
return pathToCheck;
throw new IOException("File " + pathToCheck + " is outside resource loader dir " + instanceDir +
"; set -Dsolr.allow.unsafe.resourceloading=true to allow unsafe loading");
}
/** Opens any resource by its name.
* By default, this will look in multiple locations to load the resource:
* $configDir/$resource (if resource is not absolute)
* $CWD/$resource
* otherwise, it will look for it in any jar accessible through the class loader.
* Override this method to customize loading resources.
*@return the stream for the named resource
*/
@Override
public InputStream openResource(String resource) throws IOException {
Path inConfigDir = getInstancePath().resolve("conf").resolve(resource);
if (Files.exists(inConfigDir) && Files.isReadable(inConfigDir)) {
return Files.newInputStream(checkPathIsSafe(inConfigDir));
}
Path inInstanceDir = getInstancePath().resolve(resource);
if (Files.exists(inInstanceDir) && Files.isReadable(inInstanceDir)) {
return Files.newInputStream(checkPathIsSafe(inInstanceDir));
}
// Delegate to the class loader (looking into $INSTANCE_DIR/lib jars).
// We need a ClassLoader-compatible (forward-slashes) path here!
InputStream is = classLoader.getResourceAsStream(resource.replace(File.separatorChar, '/'));
// This is a hack just for tests (it is not done in ZKResourceLoader)!
// TODO can we nuke this?
if (is == null && System.getProperty("jetty.testMode") != null) {
is = classLoader.getResourceAsStream(("conf/" + resource).replace(File.separatorChar, '/'));
}
if (is == null) {
throw new SolrResourceNotFoundException("Can't find resource '" + resource + "' in classpath or '" + instanceDir + "'");
}
return is;
}
/**
* Report the location of a resource found by the resource loader
*/
public String resourceLocation(String resource) {
Path inConfigDir = getInstancePath().resolve("conf").resolve(resource);
if (Files.exists(inConfigDir) && Files.isReadable(inConfigDir))
return inConfigDir.toAbsolutePath().normalize().toString();
Path inInstanceDir = getInstancePath().resolve(resource);
if (Files.exists(inInstanceDir) && Files.isReadable(inInstanceDir))
return inInstanceDir.toAbsolutePath().normalize().toString();
try (InputStream is = classLoader.getResourceAsStream(resource.replace(File.separatorChar, '/'))) {
if (is != null)
return "classpath:" + resource;
} catch (IOException e) {
// ignore
}
return resource;
}
/**
* Accesses a resource by name and returns the (non comment) lines
* containing data.
*
* <p>
* A comment line is any line that starts with the character "#"
* </p>
*
* @return a list of non-blank non-comment lines with whitespace trimmed
* from front and back.
* @throws IOException If there is a low-level I/O error.
*/
public List<String> getLines(String resource) throws IOException {
return getLines(resource, UTF_8);
}
/**
* Accesses a resource by name and returns the (non comment) lines containing
* data using the given character encoding.
*
* <p>
* A comment line is any line that starts with the character "#"
* </p>
*
* @param resource the file to be read
* @return a list of non-blank non-comment lines with whitespace trimmed
* @throws IOException If there is a low-level I/O error.
*/
public List<String> getLines(String resource,
String encoding) throws IOException {
return getLines(resource, Charset.forName(encoding));
}
public List<String> getLines(String resource, Charset charset) throws IOException{
try {
return WordlistLoader.getLines(openResource(resource), charset);
} catch (CharacterCodingException ex) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"Error loading resource (wrong encoding?): " + resource, ex);
}
}
/*
* A static map of short class name to fully qualified class name
*/
private static final Map<String, String> classNameCache = new ConcurrentHashMap<>();
// Using this pattern, legacy analysis components from previous Solr versions are identified and delegated to SPI loader:
private static final Pattern legacyAnalysisPattern =
Pattern.compile("((\\Q"+base+".analysis.\\E)|(\\Qsolr.\\E))([\\p{L}_$][\\p{L}\\p{N}_$]+?)(TokenFilter|Filter|Tokenizer|CharFilter)Factory");
@Override
public <T> Class<? extends T> findClass(String cname, Class<T> expectedType) {
return findClass(cname, expectedType, empty);
}
/**
* This method loads a class either with its FQN or a short-name (solr.class-simplename or class-simplename).
* It tries to load the class with the name that is given first and if it fails, it tries all the known
* solr packages. This method caches the FQN of a short-name in a static map in-order to make subsequent lookups
* for the same class faster. The caching is done only if the class is loaded by the webapp classloader and it
* is loaded using a shortname.
*
* @param cname The name or the short name of the class.
* @param subpackages the packages to be tried if the cname starts with solr.
* @return the loaded class. An exception is thrown if it fails
*/
public <T> Class<? extends T> findClass(String cname, Class<T> expectedType, String... subpackages) {
if (subpackages == null || subpackages.length == 0 || subpackages == packages) {
subpackages = packages;
String c = classNameCache.get(cname);
if (c != null) {
try {
return Class.forName(c, true, classLoader).asSubclass(expectedType);
} catch (ClassNotFoundException | ClassCastException e) {
// this can happen if the legacyAnalysisPattern below caches the wrong thing
log.warn("{} Unable to load cached class, attempting lookup. name={} shortname={} reason={}", name , c, cname, e);
classNameCache.remove(cname);
}
}
}
Class<? extends T> clazz = null;
clazz = getPackageClass(cname, expectedType);
if(clazz != null) return clazz;
try {
// first try legacy analysis patterns, now replaced by Lucene's Analysis package:
final Matcher m = legacyAnalysisPattern.matcher(cname);
if (m.matches()) {
final String name = m.group(4);
log.trace("Trying to load class from analysis SPI using name='{}'", name);
try {
if (CharFilterFactory.class.isAssignableFrom(expectedType)) {
return clazz = CharFilterFactory.lookupClass(name).asSubclass(expectedType);
} else if (TokenizerFactory.class.isAssignableFrom(expectedType)) {
return clazz = TokenizerFactory.lookupClass(name).asSubclass(expectedType);
} else if (TokenFilterFactory.class.isAssignableFrom(expectedType)) {
return clazz = TokenFilterFactory.lookupClass(name).asSubclass(expectedType);
} else {
log.warn("'{}' looks like an analysis factory, but caller requested different class type: {}", cname, expectedType.getName());
}
} catch (IllegalArgumentException ex) {
// ok, we fall back to legacy loading
}
}
// first try cname == full name
try {
return clazz = Class.forName(cname, true, classLoader).asSubclass(expectedType);
} catch (ClassNotFoundException e) {
String newName=cname;
if (newName.startsWith("solr")) {
newName = cname.substring("solr".length()+1);
}
for (String subpackage : subpackages) {
try {
String name = base + '.' + subpackage + newName;
log.trace("Trying class name {}", name);
return clazz = Class.forName(name,true,classLoader).asSubclass(expectedType);
} catch (ClassNotFoundException e1) {
// ignore... assume first exception is best.
}
}
throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, "Error loading class '" + cname + "'", e);
}
} finally {
if (clazz != null) {
//cache the shortname vs FQN if it is loaded by the webapp classloader and it is loaded
// using a shortname
if (clazz.getClassLoader() == SolrResourceLoader.class.getClassLoader() &&
!cname.equals(clazz.getName()) &&
(subpackages.length == 0 || subpackages == packages)) {
//store in the cache
classNameCache.put(cname, clazz.getName());
}
// print warning if class is deprecated
if (clazz.isAnnotationPresent(Deprecated.class)) {
DeprecationLog.log(cname,
"Solr loaded a deprecated plugin/analysis class [" + cname + "]. Please consult documentation how to replace it accordingly.");
}
}
}
}
private <T> Class<? extends T> getPackageClass(String cname, Class<T> expectedType) {
PluginInfo.ClassName cName = PluginInfo.parseClassName(cname);
if (cName.pkg == null) return null;
ResourceLoaderAware aware = CURRENT_AWARE.get();
if (aware != null) {
//this is invoked from a component
//let's check if it's a schema component
@SuppressWarnings("rawtypes")
Class type = assertAwareCompatibility(ResourceLoaderAware.class, aware);
if (schemaResourceLoaderComponents.contains(type)) {
//this is a schema component
//lets use schema classloader
return getSchemaLoader().findClass(cname, expectedType);
}
}
return null;
}
static final String[] empty = new String[0];
@Override
public <T> T newInstance(String name, Class<T> expectedType) {
return newInstance(name, expectedType, empty);
}
@SuppressWarnings({"rawtypes"})
private static final Class[] NO_CLASSES = new Class[0];
private static final Object[] NO_OBJECTS = new Object[0];
public <T> T newInstance(String cname, Class<T> expectedType, String ... subpackages) {
return newInstance(cname, expectedType, subpackages, NO_CLASSES, NO_OBJECTS);
}
@SuppressWarnings({"rawtypes"})
public <T> T newInstance(String cName, Class<T> expectedType, String [] subPackages, Class[] params, Object[] args){
Class<? extends T> clazz = findClass(cName, expectedType, subPackages);
if( clazz == null ) {
throw new SolrException( SolrException.ErrorCode.SERVER_ERROR,
"Can not find class: "+cName + " in " + classLoader);
}
T obj = null;
try {
Constructor<? extends T> constructor = null;
try {
constructor = clazz.getConstructor(params);
obj = constructor.newInstance(args);
} catch (NoSuchMethodException e) {
//look for a zero arg constructor if the constructor args do not match
try {
constructor = clazz.getConstructor();
obj = constructor.newInstance();
} catch (NoSuchMethodException e1) {
throw e;
}
}
} catch (Error err) {
log.error("Loading Class {} ({}) triggered serious java error: {}", cName, clazz.getName(),
err.getClass().getName(), err);
throw err;
} catch (Exception e) {
throw new SolrException( SolrException.ErrorCode.SERVER_ERROR,
"Error instantiating class: '" + clazz.getName()+"'", e);
}
addToCoreAware(obj);
addToResourceLoaderAware(obj);
addToInfoBeans(obj);
return obj;
}
public <T> void addToInfoBeans(T obj) {
if(!live) {
if (obj instanceof SolrInfoBean) {
infoMBeans.add((SolrInfoBean) obj);
}
}
}
public <T> boolean addToResourceLoaderAware(T obj) {
if (!live) {
if (obj instanceof ResourceLoaderAware) {
assertAwareCompatibility(ResourceLoaderAware.class, obj);
waitingForResources.add((ResourceLoaderAware) obj);
}
return true;
} else {
return false;
}
}
/** the inform() callback should be invoked on the listener.
* If this is 'live', the callback is not called so currently this returns 'false'
*
*/
public <T> boolean addToCoreAware(T obj) {
if (!live) {
if (obj instanceof SolrCoreAware) {
assertAwareCompatibility(SolrCoreAware.class, obj);
waitingForCore.add((SolrCoreAware) obj);
}
return true;
} else {
return false;
}
}
void initCore(SolrCore core) {
this.coreName = core.getName();
this.config = core.getSolrConfig();
this.coreId = core.uniqueId;
this.coreContainer = core.getCoreContainer();
SolrCore.Provider coreProvider = core.coreProvider;
this.coreReloadingClassLoader = new PackageListeningClassLoader(core.getCoreContainer(),
this, s -> config.maxPackageVersion(s), null){
@Override
protected void doReloadAction(Ctx ctx) {
log.info("Core reloading classloader issued reload for: {}/{} ", coreName, coreId);
coreProvider.reload();
}
};
core.getPackageListeners().addListener(coreReloadingClassLoader, true);
}
/**
* Tell all {@link SolrCoreAware} instances about the SolrCore
*/
public void inform(SolrCore core)
{
this.dataDir = core.getDataDir(); // removed in 9.0
this.coreName = core.getName();
this.config = core.getSolrConfig();
this.coreId = core.uniqueId;
this.coreContainer = core.getCoreContainer();
if(getSchemaLoader() != null) core.getPackageListeners().addListener(schemaLoader);
// make a copy to avoid potential deadlock of a callback calling newInstance and trying to
// add something to waitingForCore.
SolrCoreAware[] arr;
while (waitingForCore.size() > 0) {
synchronized (waitingForCore) {
arr = waitingForCore.toArray(new SolrCoreAware[waitingForCore.size()]);
waitingForCore.clear();
}
for( SolrCoreAware aware : arr) {
aware.inform( core );
}
}
// this is the last method to be called in SolrCore before the latch is released.
live = true;
}
/**
* Tell all {@link ResourceLoaderAware} instances about the loader
*/
public void inform( ResourceLoader loader ) throws IOException
{
// make a copy to avoid potential deadlock of a callback adding to the list
ResourceLoaderAware[] arr;
while (waitingForResources.size() > 0) {
synchronized (waitingForResources) {
arr = waitingForResources.toArray(new ResourceLoaderAware[waitingForResources.size()]);
waitingForResources.clear();
}
for (ResourceLoaderAware aware : arr) {
informAware(loader, aware);
}
}
}
/**
* Set the current {@link ResourceLoaderAware} object in thread local so that appropriate classloader can be used for package loaded classes
*/
public static void informAware(ResourceLoader loader, ResourceLoaderAware aware) throws IOException {
CURRENT_AWARE.set(aware);
try{
aware.inform(loader);
} finally {
CURRENT_AWARE.remove();
}
}
/**
* Register any {@link SolrInfoBean}s
* @param infoRegistry The Info Registry
*/
public void inform(Map<String, SolrInfoBean> infoRegistry) {
// this can currently happen concurrently with requests starting and lazy components
// loading. Make sure infoMBeans doesn't change.
SolrInfoBean[] arr;
synchronized (infoMBeans) {
arr = infoMBeans.toArray(new SolrInfoBean[infoMBeans.size()]);
waitingForResources.clear();
}
for (SolrInfoBean bean : arr) {
// Too slow? I suspect not, but we may need
// to start tracking this in a Set.
if (!infoRegistry.containsValue(bean)) {
try {
infoRegistry.put(bean.getName(), bean);
} catch (Exception e) {
log.warn("could not register MBean '{}'.", bean.getName(), e);
}
}
}
}
/** See {@link SolrPaths#locateSolrHome()}. */
@Deprecated
public static Path locateSolrHome() {
return SolrPaths.locateSolrHome();
}
@Deprecated
public static final String USER_FILES_DIRECTORY = "userfiles";
/**
* @return the instance path for this resource loader
*/
public Path getInstancePath() {
return instanceDir;
}
/**
* Keep a list of classes that are allowed to implement each 'Aware' interface
*/
@SuppressWarnings({"rawtypes"})
private static final Map<Class, Class[]> awareCompatibility;
static {
awareCompatibility = new HashMap<>();
awareCompatibility.put(
SolrCoreAware.class, new Class<?>[]{
// DO NOT ADD THINGS TO THIS LIST -- ESPECIALLY THINGS THAT CAN BE CREATED DYNAMICALLY
// VIA RUNTIME APIS -- UNTILL CAREFULLY CONSIDERING THE ISSUES MENTIONED IN SOLR-8311
CodecFactory.class,
DirectoryFactory.class,
ManagedIndexSchemaFactory.class,
QueryResponseWriter.class,
SearchComponent.class,
ShardHandlerFactory.class,
SimilarityFactory.class,
SolrRequestHandler.class,
UpdateRequestProcessorFactory.class
}
);
awareCompatibility.put(
ResourceLoaderAware.class, new Class<?>[]{
// DO NOT ADD THINGS TO THIS LIST -- ESPECIALLY THINGS THAT CAN BE CREATED DYNAMICALLY
// VIA RUNTIME APIS -- UNTILL CAREFULLY CONSIDERING THE ISSUES MENTIONED IN SOLR-8311
CharFilterFactory.class,
TokenFilterFactory.class,
TokenizerFactory.class,
QParserPlugin.class,
FieldType.class
}
);
}
/**If these components are trying to load classes, use schema classloader
*
*/
@SuppressWarnings("rawtypes")
private static final ImmutableSet<Class> schemaResourceLoaderComponents = ImmutableSet.of(
CharFilterFactory.class,
TokenFilterFactory.class,
TokenizerFactory.class,
FieldType.class);
/**
* Utility function to throw an exception if the class is invalid
*/
@SuppressWarnings({"rawtypes"})
public static Class assertAwareCompatibility(Class aware, Object obj) {
Class[] valid = awareCompatibility.get( aware );
if( valid == null ) {
throw new SolrException( SolrException.ErrorCode.SERVER_ERROR,
"Unknown Aware interface: "+aware );
}
for( Class v : valid ) {
if( v.isInstance( obj ) ) {
return v;
}
}
StringBuilder builder = new StringBuilder();
builder.append( "Invalid 'Aware' object: " ).append( obj );
builder.append( " -- ").append( aware.getName() );
builder.append( " must be an instance of: " );
for( Class v : valid ) {
builder.append( "[" ).append( v.getName() ).append( "] ") ;
}
throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, builder.toString() );
}
public CoreContainer getCoreContainer(){
return coreContainer;
}
public SolrConfig getSolrConfig() {
return config;
}
@Override
public void close() throws IOException {
IOUtils.close(classLoader);
}
public List<SolrInfoBean> getInfoMBeans(){
return Collections.unmodifiableList(infoMBeans);
}
/**
* Load a class using an appropriate {@link SolrResourceLoader} depending of the package on that class
* @param registerCoreReloadListener register a listener for the package and reload the core if the package is changed.
* Use this sparingly. This will result in core reloads across all the cores in
* all collections using this configset
*/
public <T> Class<? extends T> findClass( PluginInfo info, Class<T> type, boolean registerCoreReloadListener) {
if(info.cName.pkg == null) return findClass(info.className, type);
return _classLookup(info,
(Function<PackageLoader.Package.Version, Class<? extends T>>) ver -> ver.getLoader().findClass(info.cName.className, type), registerCoreReloadListener);
}
private <T> T _classLookup(PluginInfo info, Function<PackageLoader.Package.Version, T> fun, boolean registerCoreReloadListener ) {
PluginInfo.ClassName cName = info.cName;
if (registerCoreReloadListener) {
if (coreReloadingClassLoader == null) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Core not set");
}
return fun.apply(coreReloadingClassLoader.findPackageVersion(cName, true));
} else {
return fun.apply(coreReloadingClassLoader.findPackageVersion(cName, false));
}
}
/**
*Create a n instance of a class using an appropriate {@link SolrResourceLoader} depending on the package of that class
* @param registerCoreReloadListener register a listener for the package and reload the core if the package is changed.
* Use this sparingly. This will result in core reloads across all the cores in
* all collections using this configset
*/
public <T> T newInstance(PluginInfo info, Class<T> type, boolean registerCoreReloadListener) {
if(info.cName.pkg == null) {
return newInstance(info.cName.className == null?
type.getName():
info.cName.className ,
type);
}
return _classLookup( info, version -> version.getLoader().newInstance(info.cName.className, type), registerCoreReloadListener);
}
private PackageListeningClassLoader createSchemaLoader() {
CoreContainer cc = getCoreContainer();
if (cc == null) {
//corecontainer not available . can't load from packages
return null;
}
return new PackageListeningClassLoader(cc, this, pkg -> {
if (getSolrConfig() == null) return null;
return getSolrConfig().maxPackageVersion(pkg);
}, () -> {
if(getCoreContainer() == null || config == null || coreName == null || coreId==null) return;
try (SolrCore c = getCoreContainer().getCore(coreName, coreId)) {
if (c != null) {
c.fetchLatestSchema();
}
}
});
}
public static void persistConfLocally(SolrResourceLoader loader, String resourceName, byte[] content) {
// Persist locally
File confFile = new File(loader.getConfigDir(), resourceName);
try {
File parentDir = confFile.getParentFile();
if ( ! parentDir.isDirectory()) {
if ( ! parentDir.mkdirs()) {
final String msg = "Can't create managed schema directory " + parentDir.getAbsolutePath();
log.error(msg);
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg);
}
}
try (OutputStream out = new FileOutputStream(confFile);) {
out.write(content);
}
log.info("Written confile {}", resourceName);
} catch (IOException e) {
final String msg = "Error persisting conf file " + resourceName;
log.error(msg, e);
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg, e);
} finally {
try {
IOUtils.fsync(confFile.toPath(), false);
} catch (IOException e) {
final String msg = "Error syncing conf file " + resourceName;
log.error(msg, e);
}
}
}
//This is to verify if this requires to use the schema classloader for classes loaded from packages
private static final ThreadLocal<ResourceLoaderAware> CURRENT_AWARE = new ThreadLocal<>();
}