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:
Uwe Schindler 2010-01-12 20:55:21 +00:00
parent eaed8e67d2
commit a33e867922
8 changed files with 271 additions and 22 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -26,7 +26,6 @@ import java.io.Reader;
*/
public class KeywordAnalyzer extends Analyzer {
public KeywordAnalyzer() {
setOverridesTokenStreamMethod(KeywordAnalyzer.class);
}
@Override
public TokenStream tokenStream(String fieldName,

View File

@ -72,7 +72,6 @@ public class PerFieldAnalyzerWrapper extends Analyzer {
if (fieldAnalyzers != null) {
analyzerMap.putAll(fieldAnalyzers);
}
setOverridesTokenStreamMethod(PerFieldAnalyzerWrapper.class);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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&lt;BaseClass&gt; newMethod =
* new VirtualMethod&lt;BaseClass&gt;(BaseClass.class, "newName", parameters...);
* static final VirtualMethod&lt;BaseClass&gt; oldMethod =
* new VirtualMethod&lt;BaseClass&gt;(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>&gt; 1, iff {@code m1} is overridden/implemented in a subclass of the class overriding/declaring {@code m2}
* <li>&lt; 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));
}
}

View File

@ -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
}
}
}