mirror of https://github.com/apache/lucene.git
LUCENE-2188: Add a utility class for tracking deprecated overridden methods in non-final subclasses
git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@898507 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
eaed8e67d2
commit
a33e867922
|
@ -166,6 +166,10 @@ Optimizations
|
|||
directly, instead of Byte/CharBuffers, and modify CollationKeyFilter to
|
||||
take advantage of this for faster performance.
|
||||
(Steven Rowe, Uwe Schindler, Robert Muir)
|
||||
|
||||
* LUCENE-2188: Add a utility class for tracking deprecated overridden
|
||||
methods in non-final subclasses.
|
||||
(Uwe Schindler, Robert Muir)
|
||||
|
||||
Build
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ package org.apache.lucene.analysis;
|
|||
import java.io.Reader;
|
||||
import java.io.IOException;
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.apache.lucene.util.CloseableThreadLocal;
|
||||
import org.apache.lucene.util.VirtualMethod;
|
||||
import org.apache.lucene.store.AlreadyClosedException;
|
||||
|
||||
import org.apache.lucene.document.Fieldable;
|
||||
|
@ -84,22 +84,25 @@ public abstract class Analyzer implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
@Deprecated
|
||||
protected boolean overridesTokenStreamMethod = false;
|
||||
private static final VirtualMethod<Analyzer> tokenStreamMethod =
|
||||
new VirtualMethod<Analyzer>(Analyzer.class, "tokenStream", String.class, Reader.class);
|
||||
private static final VirtualMethod<Analyzer> reusableTokenStreamMethod =
|
||||
new VirtualMethod<Analyzer>(Analyzer.class, "reusableTokenStream", String.class, Reader.class);
|
||||
|
||||
/** @deprecated This is only present to preserve
|
||||
* back-compat of classes that subclass a core analyzer
|
||||
* and override tokenStream but not reusableTokenStream */
|
||||
/** This field contains if the {@link #tokenStream} method was overridden in a
|
||||
* more far away subclass of {@code Analyzer} on the current instance's inheritance path.
|
||||
* If this field is {@code true}, {@link #reusableTokenStream} should delegate to {@link #tokenStream}
|
||||
* instead of using the own implementation.
|
||||
* @deprecated Please declare all implementations of {@link #reusableTokenStream} and {@link #tokenStream}
|
||||
* as {@code final}.
|
||||
*/
|
||||
@Deprecated
|
||||
protected final boolean overridesTokenStreamMethod =
|
||||
VirtualMethod.compareImplementationDistance(this.getClass(), tokenStreamMethod, reusableTokenStreamMethod) > 0;
|
||||
|
||||
/** @deprecated This is a no-op since Lucene 3.1. */
|
||||
@Deprecated
|
||||
protected void setOverridesTokenStreamMethod(Class<? extends Analyzer> baseClass) {
|
||||
try {
|
||||
Method m = this.getClass().getMethod("tokenStream", String.class, Reader.class);
|
||||
overridesTokenStreamMethod = m.getDeclaringClass() != baseClass;
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
// cannot happen, as baseClass is subclass of Analyzer through generics
|
||||
overridesTokenStreamMethod = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import java.io.Reader;
|
|||
*/
|
||||
public class KeywordAnalyzer extends Analyzer {
|
||||
public KeywordAnalyzer() {
|
||||
setOverridesTokenStreamMethod(KeywordAnalyzer.class);
|
||||
}
|
||||
@Override
|
||||
public TokenStream tokenStream(String fieldName,
|
||||
|
|
|
@ -72,7 +72,6 @@ public class PerFieldAnalyzerWrapper extends Analyzer {
|
|||
if (fieldAnalyzers != null) {
|
||||
analyzerMap.putAll(fieldAnalyzers);
|
||||
}
|
||||
setOverridesTokenStreamMethod(PerFieldAnalyzerWrapper.class);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -58,11 +58,6 @@ public abstract class StopwordAnalyzerBase extends ReusableAnalyzerBase {
|
|||
* the analyzer's stopword set
|
||||
*/
|
||||
protected StopwordAnalyzerBase(final Version version, final Set<?> stopwords) {
|
||||
/*
|
||||
* no need to call
|
||||
* setOverridesTokenStreamMethod(StopwordAnalyzerBase.class); here, both
|
||||
* tokenStream methods are final in this class.
|
||||
*/
|
||||
matchVersion = version;
|
||||
// analyzers should use char array set for stopwords!
|
||||
this.stopwords = stopwords == null ? CharArraySet.EMPTY_SET : CharArraySet
|
||||
|
|
|
@ -71,7 +71,6 @@ public class StandardAnalyzer extends Analyzer {
|
|||
* @param stopWords stop words */
|
||||
public StandardAnalyzer(Version matchVersion, Set<?> stopWords) {
|
||||
stopSet = stopWords;
|
||||
setOverridesTokenStreamMethod(StandardAnalyzer.class);
|
||||
replaceInvalidAcronym = matchVersion.onOrAfter(Version.LUCENE_24);
|
||||
this.matchVersion = matchVersion;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
package org.apache.lucene.util;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A utility for keeping backwards compatibility on previously abstract methods
|
||||
* (or similar replacements).
|
||||
* <p>Before the replacement method can be made abstract, the old method must kept deprecated.
|
||||
* If somebody still overrides the deprecated method in a non-final class,
|
||||
* you must keep track, of this and maybe delegate to the old method in the subclass.
|
||||
* The cost of reflection is minimized by the following usage of this class:</p>
|
||||
* <p>Define <strong>static final</strong> fields in the base class ({@code BaseClass}),
|
||||
* where the old and new method are declared:</p>
|
||||
* <pre>
|
||||
* static final VirtualMethod<BaseClass> newMethod =
|
||||
* new VirtualMethod<BaseClass>(BaseClass.class, "newName", parameters...);
|
||||
* static final VirtualMethod<BaseClass> oldMethod =
|
||||
* new VirtualMethod<BaseClass>(BaseClass.class, "oldName", parameters...);
|
||||
* </pre>
|
||||
* <p>This enforces the singleton status of these objects, as the maintenance of the cache would be too costly else.
|
||||
* If you try to create a second instance of for the same method/{@code baseClass} combination, an exception is thrown.
|
||||
* <p>To detect if e.g. the old method was overridden by a more far subclass on the inheritance path to the current
|
||||
* instance's class, use a <strong>non-static</strong> field:</p>
|
||||
* <pre>
|
||||
* final boolean isDeprecatedMethodOverridden =
|
||||
* oldMethod.getImplementationDistance(this.getClass()) > newMethod.getImplementationDistance(this.getClass());
|
||||
*
|
||||
* <em>// alternatively (more readable):</em>
|
||||
* final boolean isDeprecatedMethodOverridden =
|
||||
* VirtualMethod.compareImplementationDistance(this.getClass(), oldMethod, newMethod) > 0
|
||||
* </pre>
|
||||
* <p>{@link #getImplementationDistance} returns the distance of the subclass that overrides this method.
|
||||
* The one with the larger distance should be used preferable.
|
||||
* This way also more complicated method rename scenarios can be handled
|
||||
* (think of 2.9 {@code TokenStream} deprecations).</p>
|
||||
*/
|
||||
public final class VirtualMethod<C> {
|
||||
|
||||
private static final Set<Method> singletonSet = Collections.synchronizedSet(new HashSet<Method>());
|
||||
|
||||
private final Class<C> baseClass;
|
||||
private final String method;
|
||||
private final Class<?>[] parameters;
|
||||
private final IdentityHashMap<Class<? extends C>, Integer> cache =
|
||||
new IdentityHashMap<Class<? extends C>, Integer>();
|
||||
|
||||
/**
|
||||
* Creates a new instance for the given {@code baseClass} and method declaration.
|
||||
* @throws UnsupportedOperationException if you create a second instance of the same
|
||||
* {@code baseClass} and method declaration combination. This enforces the singleton status.
|
||||
* @throws IllegalArgumentException if {@code baseClass} does not declare the given method.
|
||||
*/
|
||||
public VirtualMethod(Class<C> baseClass, String method, Class<?>... parameters) {
|
||||
this.baseClass = baseClass;
|
||||
this.method = method;
|
||||
this.parameters = parameters;
|
||||
try {
|
||||
if (!singletonSet.add(baseClass.getDeclaredMethod(method, parameters)))
|
||||
throw new UnsupportedOperationException(
|
||||
"VirtualMethod instances must be singletons and therefore " +
|
||||
"assigned to static final members in the same class, they use as baseClass ctor param."
|
||||
);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
throw new IllegalArgumentException(baseClass.getName() + " has no such method: "+nsme.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance from the {@code baseClass} in which this method is overridden/implemented
|
||||
* in the inheritance path between {@code baseClass} and the given subclass {@code subclazz}.
|
||||
* @return 0 iff not overridden, else the distance to the base class
|
||||
*/
|
||||
public synchronized int getImplementationDistance(final Class<? extends C> subclazz) {
|
||||
Integer distance = cache.get(subclazz);
|
||||
if (distance == null) {
|
||||
cache.put(subclazz, distance = Integer.valueOf(reflectImplementationDistance(subclazz)));
|
||||
}
|
||||
return distance.intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, if this method is overridden/implemented in the inheritance path between
|
||||
* {@code baseClass} and the given subclass {@code subclazz}.
|
||||
* <p>You can use this method to detect if a method that should normally be final was overridden
|
||||
* by the given instance's class.
|
||||
* @return {@code false} iff not overridden
|
||||
*/
|
||||
public boolean isOverriddenAsOf(final Class<? extends C> subclazz) {
|
||||
return getImplementationDistance(subclazz) > 0;
|
||||
}
|
||||
|
||||
private int reflectImplementationDistance(final Class<? extends C> subclazz) {
|
||||
if (!baseClass.isAssignableFrom(subclazz))
|
||||
throw new IllegalArgumentException(subclazz.getName() + " is not a subclass of " + baseClass.getName());
|
||||
boolean overridden = false;
|
||||
int distance = 0;
|
||||
for (Class<?> clazz = subclazz; clazz != baseClass && clazz != null; clazz = clazz.getSuperclass()) {
|
||||
// lookup method, if success mark as overridden
|
||||
if (!overridden) {
|
||||
try {
|
||||
clazz.getDeclaredMethod(method, parameters);
|
||||
overridden = true;
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
}
|
||||
}
|
||||
|
||||
// increment distance if overridden
|
||||
if (overridden) distance++;
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that compares the implementation/override distance of two methods.
|
||||
* @return <ul>
|
||||
* <li>> 1, iff {@code m1} is overridden/implemented in a subclass of the class overriding/declaring {@code m2}
|
||||
* <li>< 1, iff {@code m2} is overridden in a subclass of the class overriding/declaring {@code m1}
|
||||
* <li>0, iff both methods are overridden in the same class (or are not overridden at all)
|
||||
* </ul>
|
||||
*/
|
||||
public static <C> int compareImplementationDistance(final Class<? extends C> clazz,
|
||||
final VirtualMethod<C> m1, final VirtualMethod<C> m2)
|
||||
{
|
||||
return Integer.valueOf(m1.getImplementationDistance(clazz)).compareTo(m2.getImplementationDistance(clazz));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package org.apache.lucene.util;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
public class TestVirtualMethod extends LuceneTestCase {
|
||||
|
||||
private static final VirtualMethod<TestVirtualMethod> publicTestMethod =
|
||||
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "publicTest", String.class);
|
||||
private static final VirtualMethod<TestVirtualMethod> protectedTestMethod =
|
||||
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "protectedTest", int.class);
|
||||
|
||||
public void publicTest(String test) {}
|
||||
protected void protectedTest(int test) {}
|
||||
|
||||
static class TestClass1 extends TestVirtualMethod {
|
||||
@Override
|
||||
public void publicTest(String test) {}
|
||||
@Override
|
||||
protected void protectedTest(int test) {}
|
||||
}
|
||||
|
||||
static class TestClass2 extends TestClass1 {
|
||||
@Override // make it public here
|
||||
public void protectedTest(int test) {}
|
||||
}
|
||||
|
||||
static class TestClass3 extends TestClass2 {
|
||||
@Override
|
||||
public void publicTest(String test) {}
|
||||
}
|
||||
|
||||
static class TestClass4 extends TestVirtualMethod {
|
||||
}
|
||||
|
||||
static class TestClass5 extends TestClass4 {
|
||||
}
|
||||
|
||||
public void test() {
|
||||
assertEquals(0, publicTestMethod.getImplementationDistance(this.getClass()));
|
||||
assertEquals(1, publicTestMethod.getImplementationDistance(TestClass1.class));
|
||||
assertEquals(1, publicTestMethod.getImplementationDistance(TestClass2.class));
|
||||
assertEquals(3, publicTestMethod.getImplementationDistance(TestClass3.class));
|
||||
assertFalse(publicTestMethod.isOverriddenAsOf(TestClass4.class));
|
||||
assertFalse(publicTestMethod.isOverriddenAsOf(TestClass5.class));
|
||||
|
||||
assertEquals(0, protectedTestMethod.getImplementationDistance(this.getClass()));
|
||||
assertEquals(1, protectedTestMethod.getImplementationDistance(TestClass1.class));
|
||||
assertEquals(2, protectedTestMethod.getImplementationDistance(TestClass2.class));
|
||||
assertEquals(2, protectedTestMethod.getImplementationDistance(TestClass3.class));
|
||||
assertFalse(protectedTestMethod.isOverriddenAsOf(TestClass4.class));
|
||||
assertFalse(protectedTestMethod.isOverriddenAsOf(TestClass5.class));
|
||||
|
||||
assertTrue(VirtualMethod.compareImplementationDistance(TestClass3.class, publicTestMethod, protectedTestMethod) > 0);
|
||||
assertEquals(0, VirtualMethod.compareImplementationDistance(TestClass5.class, publicTestMethod, protectedTestMethod));
|
||||
|
||||
try {
|
||||
// cast to Class to remove generics:
|
||||
@SuppressWarnings("unchecked") int dist = publicTestMethod.getImplementationDistance((Class) LuceneTestCase.class);
|
||||
fail("LuceneTestCase is not a subclass and can never override publicTest(String)");
|
||||
} catch (IllegalArgumentException arg) {
|
||||
// pass
|
||||
}
|
||||
|
||||
try {
|
||||
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "bogus");
|
||||
fail("Method bogus() does not exist, so IAE should be thrown");
|
||||
} catch (IllegalArgumentException arg) {
|
||||
// pass
|
||||
}
|
||||
|
||||
try {
|
||||
new VirtualMethod<TestClass2>(TestClass2.class, "publicTest", String.class);
|
||||
fail("Method publicTest(String) is not declared in TestClass2, so IAE should be thrown");
|
||||
} catch (IllegalArgumentException arg) {
|
||||
// pass
|
||||
}
|
||||
|
||||
try {
|
||||
// try to create a second instance of the same baseClass / method combination
|
||||
new VirtualMethod<TestVirtualMethod>(TestVirtualMethod.class, "publicTest", String.class);
|
||||
fail("Violating singleton status succeeded");
|
||||
} catch (UnsupportedOperationException arg) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue