diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/AnalysisSPILoader.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/AnalysisSPILoader.java index f45d5d4626d..1665137ecd6 100644 --- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/AnalysisSPILoader.java +++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/AnalysisSPILoader.java @@ -33,8 +33,9 @@ import org.apache.lucene.util.SPIClassIterator; */ public final class AnalysisSPILoader { - private final Map> services; + private volatile Map> services = Collections.emptyMap(); private final Class clazz; + private final String[] suffixes; public AnalysisSPILoader(Class clazz) { this(clazz, new String[] { clazz.getSimpleName() }); @@ -50,6 +51,22 @@ public final class AnalysisSPILoader { public AnalysisSPILoader(Class clazz, String[] suffixes, ClassLoader classloader) { this.clazz = clazz; + this.suffixes = suffixes; + reload(classloader); + } + + /** + * Reloads the internal SPI list from the given {@link ClassLoader}. + * Changes to the service list are visible after the method ends, all + * iterators (e.g., from {@link #availableServices()},...) stay consistent. + * + *

NOTE: Only new service providers are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new service providers on the given classpath/classloader! + */ + public void reload(ClassLoader classloader) { final SPIClassIterator loader = SPIClassIterator.get(clazz, classloader); final LinkedHashMap> services = new LinkedHashMap>(); while (loader.hasNext()) { @@ -69,6 +86,11 @@ public final class AnalysisSPILoader { // only add the first one for each name, later services will be ignored // this allows to place services before others in classpath to make // them used instead of others + // + // TODO: Should we disallow duplicate names here? + // Allowing it may get confusing on collisions, as different packages + // could contain same factory class, which is a naming bug! + // When changing this be careful to allow reload()! if (!services.containsKey(name)) { services.put(name, service); } diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/CharFilterFactory.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/CharFilterFactory.java index 489d34f629a..63b5405e8c3 100644 --- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/CharFilterFactory.java +++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/CharFilterFactory.java @@ -29,16 +29,7 @@ import org.apache.lucene.analysis.CharFilter; public abstract class CharFilterFactory extends AbstractAnalysisFactory { private static final AnalysisSPILoader loader = - getSPILoader(Thread.currentThread().getContextClassLoader()); - - /** - * Used by e.g. Apache Solr to get a correctly configured instance - * of {@link AnalysisSPILoader} from Solr's classpath. - * @lucene.internal - */ - public static AnalysisSPILoader getSPILoader(ClassLoader classloader) { - return new AnalysisSPILoader(CharFilterFactory.class, classloader); - } + new AnalysisSPILoader(CharFilterFactory.class); /** looks up a charfilter by name from context classpath */ public static CharFilterFactory forName(String name) { @@ -55,5 +46,21 @@ public abstract class CharFilterFactory extends AbstractAnalysisFactory { return loader.availableServices(); } + /** + * Reloads the factory list from the given {@link ClassLoader}. + * Changes to the factories are visible after the method ends, all + * iterators ({@link #availableCharFilters()},...) stay consistent. + * + *

NOTE: Only new factories are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new factories on the given classpath/classloader! + */ + public static void reloadCharFilters(ClassLoader classloader) { + loader.reload(classloader); + } + + /** Wraps the given Reader with a CharFilter. */ public abstract Reader create(Reader input); } diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenFilterFactory.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenFilterFactory.java index 8e463aaa0d1..324a919e4bc 100644 --- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenFilterFactory.java +++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenFilterFactory.java @@ -28,17 +28,8 @@ import org.apache.lucene.analysis.TokenStream; public abstract class TokenFilterFactory extends AbstractAnalysisFactory { private static final AnalysisSPILoader loader = - getSPILoader(Thread.currentThread().getContextClassLoader()); - - /** - * Used by e.g. Apache Solr to get a correctly configured instance - * of {@link AnalysisSPILoader} from Solr's classpath. - * @lucene.internal - */ - public static AnalysisSPILoader getSPILoader(ClassLoader classloader) { - return new AnalysisSPILoader(TokenFilterFactory.class, - new String[] { "TokenFilterFactory", "FilterFactory" }, classloader); - } + new AnalysisSPILoader(TokenFilterFactory.class, + new String[] { "TokenFilterFactory", "FilterFactory" }); /** looks up a tokenfilter by name from context classpath */ public static TokenFilterFactory forName(String name) { @@ -55,6 +46,21 @@ public abstract class TokenFilterFactory extends AbstractAnalysisFactory { return loader.availableServices(); } + /** + * Reloads the factory list from the given {@link ClassLoader}. + * Changes to the factories are visible after the method ends, all + * iterators ({@link #availableTokenFilters()},...) stay consistent. + * + *

NOTE: Only new factories are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new factories on the given classpath/classloader! + */ + public static void reloadTokenFilters(ClassLoader classloader) { + loader.reload(classloader); + } + /** Transform the specified input TokenStream */ public abstract TokenStream create(TokenStream input); } diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenizerFactory.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenizerFactory.java index 7ac94c71766..5050936080e 100644 --- a/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenizerFactory.java +++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/util/TokenizerFactory.java @@ -29,16 +29,7 @@ import java.util.Set; public abstract class TokenizerFactory extends AbstractAnalysisFactory { private static final AnalysisSPILoader loader = - getSPILoader(Thread.currentThread().getContextClassLoader()); - - /** - * Used by e.g. Apache Solr to get a correctly configured instance - * of {@link AnalysisSPILoader} from Solr's classpath. - * @lucene.internal - */ - public static AnalysisSPILoader getSPILoader(ClassLoader classloader) { - return new AnalysisSPILoader(TokenizerFactory.class, classloader); - } + new AnalysisSPILoader(TokenizerFactory.class); /** looks up a tokenizer by name from context classpath */ public static TokenizerFactory forName(String name) { @@ -55,6 +46,21 @@ public abstract class TokenizerFactory extends AbstractAnalysisFactory { return loader.availableServices(); } + /** + * Reloads the factory list from the given {@link ClassLoader}. + * Changes to the factories are visible after the method ends, all + * iterators ({@link #availableTokenizers()},...) stay consistent. + * + *

NOTE: Only new factories are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new factories on the given classpath/classloader! + */ + public static void reloadTokenizers(ClassLoader classloader) { + loader.reload(classloader); + } + /** Creates a TokenStream of the specified input */ public abstract Tokenizer create(Reader input); } diff --git a/lucene/core/src/java/org/apache/lucene/codecs/Codec.java b/lucene/core/src/java/org/apache/lucene/codecs/Codec.java index bc8715da9b2..c7f46e452ce 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/Codec.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/Codec.java @@ -86,6 +86,21 @@ public abstract class Codec implements NamedSPILoader.NamedSPI { return loader.availableServices(); } + /** + * Reloads the codec list from the given {@link ClassLoader}. + * Changes to the codecs are visible after the method ends, all + * iterators ({@link #availableCodecs()},...) stay consistent. + * + *

NOTE: Only new codecs are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new codecs on the given classpath/classloader! + */ + public static void reloadCodecs(ClassLoader classloader) { + loader.reload(classloader); + } + private static Codec defaultCodec = Codec.forName("Lucene40"); /** expert: returns the default codec used for newly created diff --git a/lucene/core/src/java/org/apache/lucene/codecs/PostingsFormat.java b/lucene/core/src/java/org/apache/lucene/codecs/PostingsFormat.java index 9089e29b7be..7987183a8c7 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/PostingsFormat.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/PostingsFormat.java @@ -70,4 +70,19 @@ public abstract class PostingsFormat implements NamedSPILoader.NamedSPI { public static Set availablePostingsFormats() { return loader.availableServices(); } + + /** + * Reloads the postings format list from the given {@link ClassLoader}. + * Changes to the postings formats are visible after the method ends, all + * iterators ({@link #availablePostingsFormats()},...) stay consistent. + * + *

NOTE: Only new postings formats are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new postings formats on the given classpath/classloader! + */ + public static void reloadPostingsFormats(ClassLoader classloader) { + loader.reload(classloader); + } } diff --git a/lucene/core/src/java/org/apache/lucene/util/NamedSPILoader.java b/lucene/core/src/java/org/apache/lucene/util/NamedSPILoader.java index 54173afb8fa..a067bde5f97 100644 --- a/lucene/core/src/java/org/apache/lucene/util/NamedSPILoader.java +++ b/lucene/core/src/java/org/apache/lucene/util/NamedSPILoader.java @@ -28,16 +28,34 @@ import java.util.ServiceConfigurationError; * Helper class for loading named SPIs from classpath (e.g. Codec, PostingsFormat). * @lucene.internal */ -// TODO: would be nice to have case insensitive lookups. public final class NamedSPILoader implements Iterable { - private final Map services; + private volatile Map services = Collections.emptyMap(); private final Class clazz; public NamedSPILoader(Class clazz) { + this(clazz, Thread.currentThread().getContextClassLoader()); + } + + public NamedSPILoader(Class clazz, ClassLoader classloader) { this.clazz = clazz; - final SPIClassIterator loader = SPIClassIterator.get(clazz); - final LinkedHashMap services = new LinkedHashMap(); + reload(classloader); + } + + /** + * Reloads the internal SPI list from the given {@link ClassLoader}. + * Changes to the service list are visible after the method ends, all + * iterators ({@link #iterator()},...) stay consistent. + * + *

NOTE: Only new service providers are added, existing ones are + * never removed or replaced. + * + *

This method is expensive and should only be called for discovery + * of new service providers on the given classpath/classloader! + */ + public void reload(ClassLoader classloader) { + final LinkedHashMap services = new LinkedHashMap(this.services); + final SPIClassIterator loader = SPIClassIterator.get(clazz, classloader); while (loader.hasNext()) { final Class c = loader.next(); try { diff --git a/solr/core/src/java/org/apache/solr/core/SolrConfig.java b/solr/core/src/java/org/apache/solr/core/SolrConfig.java index ba242bc96cf..e68f904a543 100644 --- a/solr/core/src/java/org/apache/solr/core/SolrConfig.java +++ b/solr/core/src/java/org/apache/solr/core/SolrConfig.java @@ -436,9 +436,7 @@ public class SolrConfig extends Config { */ public List getPluginInfos(String type){ List result = pluginStore.get(type); - return result == null ? - (List) Collections.EMPTY_LIST: - result; + return result == null ? Collections.emptyList(): result; } public PluginInfo getPluginInfo(String type){ List result = pluginStore.get(type); @@ -446,29 +444,31 @@ public class SolrConfig extends Config { } private void initLibs() { - NodeList nodes = (NodeList) evaluate("lib", XPathConstants.NODESET); - if (nodes==null || nodes.getLength()==0) - return; + if (nodes == null || nodes.getLength() == 0) return; log.info("Adding specified lib dirs to ClassLoader"); - for (int i=0; iMUST * only be called prior to using this ResourceLoader to get any resources, otherwise - * it's behavior will be non-deterministic. + * it's behavior will be non-deterministic. You also have to {link @reloadLuceneSPI} + * before using this ResourceLoader. * * @param baseDir base directory whose children (either jars or directories of * classes) will be in the classpath, will be resolved relative @@ -150,7 +148,8 @@ public class SolrResourceLoader implements ResourceLoader * Adds the specific file/dir specified to the ClassLoader used by this * ResourceLoader. This method MUST * only be called prior to using this ResourceLoader to get any resources, otherwise - * it's behavior will be non-deterministic. + * it's behavior will be non-deterministic. You also have to {link #reloadLuceneSPI()} + * before using this ResourceLoader. * * @param path A jar file (or directory of classes) to be added to the classpath, * will be resolved relative the instance dir. @@ -169,6 +168,22 @@ public class SolrResourceLoader implements ResourceLoader } } + /** + * Reloads all Lucene SPI implementations using the new classloader. + * This method must be called after {@link #addToClassLoader(String)} + * and {@link #addToClassLoader(String,FileFilter)} before using + * this ResourceLoader. + */ + void reloadLuceneSPI() { + // Codecs: + PostingsFormat.reloadPostingsFormats(this.classLoader); + Codec.reloadCodecs(this.classLoader); + // Analysis: + CharFilterFactory.reloadCharFilters(this.classLoader); + TokenFilterFactory.reloadTokenFilters(this.classLoader); + TokenizerFactory.reloadTokenizers(this.classLoader); + } + private static URLClassLoader replaceClassLoader(final URLClassLoader oldLoader, final File base, final FileFilter filter) { @@ -351,9 +366,6 @@ public class SolrResourceLoader implements ResourceLoader */ private static final Map classNameCache = new ConcurrentHashMap(); - // A static map of AnalysisSPILoaders, keyed by ClassLoader used (because it can change during Solr lifetime) and expected base class: - private static final WeakIdentityMap,AnalysisSPILoader>> expectedTypesSPILoaders = WeakIdentityMap.newConcurrentHashMap(); - // 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)|(\\Q"+project+".\\E))([\\p{L}_$][\\p{L}\\p{N}_$]+?)(TokenFilter|Filter|Tokenizer|CharFilter)Factory"); @@ -388,24 +400,20 @@ public class SolrResourceLoader implements ResourceLoader // first try legacy analysis patterns, now replaced by Lucene's Analysis package: final Matcher m = legacyAnalysisPattern.matcher(cname); if (m.matches()) { - log.trace("Trying to load class from analysis SPI"); - // retrieve the map of classLoader -> expectedType -> SPI from cache / regenerate cache - Map,AnalysisSPILoader> spiLoaders = expectedTypesSPILoaders.get(classLoader); - if (spiLoaders == null) { - spiLoaders = new IdentityHashMap,AnalysisSPILoader>(3); - spiLoaders.put(CharFilterFactory.class, CharFilterFactory.getSPILoader(classLoader)); - spiLoaders.put(TokenizerFactory.class, TokenizerFactory.getSPILoader(classLoader)); - spiLoaders.put(TokenFilterFactory.class, TokenFilterFactory.getSPILoader(classLoader)); - expectedTypesSPILoaders.put(classLoader, spiLoaders); - } - final AnalysisSPILoader loader = spiLoaders.get(expectedType); - if (loader != null) { - // it's a correct expected type for analysis! Let's go on! - try { - return clazz = loader.lookupClass(m.group(4)).asSubclass(expectedType); - } catch (IllegalArgumentException ex) { - // ok, we fall back to legacy loading + final String name = m.group(4); + log.trace("Trying to load class from analysis SPI using name='{}'", name); + try { + if (CharFilterFactory.class == expectedType) { + return clazz = CharFilterFactory.lookupClass(name).asSubclass(expectedType); + } else if (TokenizerFactory.class == expectedType) { + return clazz = TokenizerFactory.lookupClass(name).asSubclass(expectedType); + } else if (TokenFilterFactory.class == 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 } }